ISBN: 1-86100-735-3

Текст
                    .NET
Сетевое программирование
для профессионалов
Эндрю Кровчик, Винод Кумар, Номан Лагари,
Аджит Мунгале, Кристиан Нагел Тим Паркер
Шриниваса Шивакумар
wrox
ER ТО PROGRAMMER*
Адрес группы технической поддержки издательства Wrox: support@wrox.com
Обновления и исходный код доступны на сайте’ www wrox.com
Поддержка разработчиков: p2p.wrox.com

Professional .NET Network Programming Andrew Krawczyk Vinod Kumar Nauman Laghari Ajit Mungale Christian Nagel Tim Parker Srinivasa Sivakumar Wrox Press Ltd.®
.NET Сетевое программирование для профессионалов Эндрю Кровчик Винод Кумар Номан Лагари Аджит Мунгале Кристиан Нагел Тим Паркер Шриниваса Шивакумар Издательство “ЛОРИ”
♦ * t Andrew Krowc/vk Vinod Kumar, Nauman Laghari Ajit Mungale. Christian Nagel. Tim Parker, Srinivasa Sivakurnar Prpfessinal .NET Network Programming Copyright © 2002 Urox Press Published by Wrox Press Ltd., Arden House, 1102 Warwick Road, Acocks Green, Birmingham, B27 GBH, United Kingdom Printed in the United States of America ISBN 1-86100-735-3 Эндрю Кровчик, Винод Кумар, Номан Лагари, Аджит Мунгале, Кристиан Нател, Тим Паркер, Шриниваса Шивакумар .NET. Сетевое программирование для профессионалов Переводчик Вл. Стрельцов Научный редактор Н. Смольянинов © Издательство "Лори", 2005 Изд. №: OAI (03) ЛР№: 070612 30.09.97 г. ISBN 5-85582-170-2 Подписано в печать 24.03.2005. Формат 70x100/16 Гарнитура Нью-Баскервиль. Печат ь офсегная. Печ, л. 26. Тираж 1500. Заказ N 810$ Цена договорная. Издательство "Лори" Москва, 123100, Шмитовский пр., д. 13/6, стр. 1 (ном. ТАРП ЦАО) Телефоны для оптовых покупателей: (095) 256-02-83 Размещение рекламы: (095) 259-01-62 www.lory-press.ru Отпечатано в типографии ООО "Тиль-2004" Москва, Дербеневская наб., д.7
Содержание Об авторах xi Введение xiii Основные темы книги Предполагаемый круг читателей Что необходимо для работы с книгой Соглашения о стилях Как скопировать код примеров для этой книги xiii XV XV XV xvi ГЛАВА 1. Сетевые понятия и протоколы 4 Физическая сеть Линии глобальной сети Протокол Ethernet Физические компоненты Многоуровневая модель OSI Уровень 1: физический уровень Уровень 2: канальный уровень Уровень 3: сетевой уровень Уровень 4: транспортный уровень Уровень 5: сеансовый уровень Уровень 6: представительский уровень Уровень 7: прикладной уровень Сетевые протоколы Базовые протоколы Протоколы Интернета Протоколы электронной почты Другие прикладные протоколы 2 2 3 5 10 12 12 13 13 14 14 14 14 15 25 29 30
vi Сокеты 31 Имена доменов 31 Служба whois 33 Серверы доменных имен 33 Интернет 34 Интрасети и экстрасети 34 Брандмауэры 34 Web-прокси 35 'J XML Web-сервисы 36 ,* Передача сообщений 38 Другие способы доступа к сетевым объектам 38 Организации и стандарты Интернета 39 Итоги 40 ГЛАВА 2. Потоки в.NET 41 . Потоки в .NET 41 Синхронный и асинхронный ввод/вывод 42 Класс Stream 42 Класс FileStream 46 Чтение и запись в классе FileStream 49 Класс BufferedStream 53 Класс Memorystream 53 Класс NetworkStream 54 Класс CryptoStream 57 Обработка потоков 59 Кодирование строковых данных 60 Двоичные файлы 62 TextReader 65 TextWriter 67 Сериализация 69 Сериализация в формат XML 70 Сериализация с помощью объектов форматирования 72 Итоги 76 ГЛАВА 3. Сетевое программирование в .NET 77 Классы пространства System.Net—обзор 77 Поиск имен 78 IP-адреса 78 Аутентификация и авторизация 78 Запросы и ответы 78 . Управление соединениями 79 Записи cookies 80 Прокси-сервер 80 Сокеты 80 Работа с URI 81 Класс Uri 82 Свойства класса Uri 83 Изменение URI с помощью класса UriBuilder 85 Абсолютные и относительные URI 85
Содержание vii IP-адреса Предопределенные адреса Порядок байтов, используемый в хосте и сети Класс Dns Разрешение имени в IP-адрес Как разрешается 1Р-адрес? Асинхронное разрешение IP-адреса Запросы и ответы WebRequest и WebResponse Подключаемые протоколы FileWebRequest и FtleWebResponse Формирование пула соединений Использование Web-прокси Класс WebProxy Web-прокси по умолчанию Изменение WebProxy для конкретных запросов Аутентификация Разрешения Использование атрибутов разрешения Конфигурирование разрешений Итоги 86 87 87 88 88 89 91 92 93 / 95 95 98 98 99 100 100 101 101 102 105 107 ГЛАВА 4. Работа с сокетами 109 Сокеты Типы сокетов Работа с сокетами в .NET Класс System.Net.Sockets.Socket Создание приложения на потоковом сокете TCP Управление исключениями в System.Net.Sockets Опции сокетов Асинхронное программирование Асинхронное приложение-клиент Асинхронное приложение-сервер Разрешения сокетов Итоги 109 110 112 113 114 122 125 127 128 133 137 143 ГЛАВА 5. TCP 145 Обзор TCP Инкапсуляция Терминология TCP Заголовки TCP Соединения TCP Операции TCP Введение в TCP на платформе .NET Класс TcpCIient Построение реального приложения на сокетах Реализация класса FtpWebRequest Класс TcpLIstener 145 145 146 146 147 148 । 149 150 155 161 175
viii .NET Remoting 182 Домены приложений 183 Кок работает Remoting 183 Итоги 191 ГЛАВА 6. UDP 192 Обзор протокола UDP 192 UDPb.NET 199 Класс UdpCIient 200 Приложение интерактивного форума, использующее UDP 210 Приложение передачи файла 214 Файловый сервер 214 Приемник файла 217 Широковещательная передача 219 Высокоуровневые протоколы, базирующиеся на UDP 220 Итоги 221 ГЛАВА 7. Сокеты групповой рассылки 222 Однонаправленные, широковещательные и групповые передачи 223 Модели приложений с групповой рассылкой 224 Архитектура сокетов групповой рассылки 225 Протокол IGMP 226 Групповые адреса 227 Масштабируемость 232 Надежность 232 Безопасность 233 Использование сокетов групповой рассылки в .NET 233 Отправитель ' 233 Получатель ' 234 Создание приложения интерактивного форума 235 Пользовательский интерфейс 235 Параметры конфигурирования 237 Присоединение к группе, получающей рассылку ‘ 238 Получение сообщений, адресованных группе 239 Отправка групповых сообщений 240 Прекращение членства в группе 240 Запуск приложения интерактивного форума 241 Приложение демонстрации изображений 241 Реализация демонстрации изображений 242 Создание протокола для изображений 242 Сервер демонстрации изображений 247 < Клиент приложения демонстрации изображений 257 Итоги 263 ГЛАВА 8. НИР 264 Обзор протокола HTTP 264 HTTP-заголовки 266 HTTP-запросы 268 HTTP-ответы 270
Содержание / ix НИР в.NET 271 HttpWebRequest и HttpWebResponse 272 Приложение перевода валют 274 Отсылка данных на сервер 276 Передача данных порциями в HTTP 277 Поддержка активного соединения HTTP 279 Управление соединением HTTP 279 Класс WebClient 281 Аутентификация 286 Поддержка прокси-сервера 288 Чтение и запись cookie 289 HTTP-сервер с поддержкой ASP.NET 293 Конфигурационные файлы сервера 293 Кодирование сервера 294 НИР и .NET Remoting t 304 Построение простого приложения для среды Remoting ’ 304 Итоги 307 ГЛАВА 9. Протоколы электронной почты 308 Об электронной почте коротко 308 Как работает электронная почта 309 Протоколы электронной почты 310 SMTP 310 Типичное сообщение электронной почты 314 Просмотр заголовков в Outlook 317 Как обстоит дело с MIME? 317 Получение электронной почты в системе клиент-сервер 320 .NET и электронная почта 324 SMTP 324 Приложение почтового SMTP-клиента 328 POP3 329 NNTP 336 Итоги 344 ГЛАВА 10. Криптография в .NET 345 История криптографии 345 Что такое криптография 346 Зачем нужна криптография 347 Концепции криптографии 348 Криптографические алгоритмы 349 Симметричные алгоритмы 349 Асимметричные алгоритмы 351 Алгоритмы дайджеста сообщения 352 Цифровые подписи 353 Криптографическая терминология 354 Блочные и поточные шифры 354 Заполнение 354 Режимы 355
Пространство имен System.Security .Cryptography 356 Иерархия криптографии классов 357 Хеширование в .NET 358 Класс HashAlgorithm 358 Симметрические преобразования в .NET 364 Класс SymmetricAlgorithm 364 Использование других симметричных алгоритмов 368 Асимметричные преобразования в .NET 369 Класс AsymmetricAlgorithm 369 Использование алгоритма RSA 370 Загрузка открытых и закрытых ключей 373 Чтение сертификата Х509 375 Криптография и сетевое программирование 377 Итоги 382 ГЛАВА 11. Протоколы аутентификации 383 Протоколы аутентификации 383 NTLM 384 Kerberos 388 Безопасность в .NET и Windows * 393 Интерфейс System.Net.lAuthenticationModule 398 Итоги 400
Об авторах Эндрю Кровчик Эндрю Кровчик — консультант по разработке программного обеспечения, в на- стоящее время с головой ушел в разработку платформы .NET. Закоренелый фанат Web-сервисов, Эндрю тратит значительное время на написание и рецензирование книги статей, посвященных платформе .NET, для издательства Wrox Press. Кроме того, он преподает в Колледже Элмхерста (Элмхерст, штат Иллинойс), читает лек- ции на вечерних курсах. Свободное время (когда оно вдруг появляется) Эндрю любит проводить с женой Элеонор и собакой Луи, повозиться со своим “мустангом" выпуска 1967 г. С Эндрю можно связаться через его почтовый ящик Krowczyk@i-netway.com. Винод Кумар Винод Кумар — автор, разработчик и технический рецензент в одном лице. Он специализируется на Web- и мобильных технологиях, использующих решения Mic- rosoft. В настоящее время работает в индийском городе Ченнай в компании Emerald Software Ltd. Винод — ведущий автор готовящейся к изданию книги “Mobile Applica- tion development with .NET”, написал много технических статей для таких сайтов, как ASPToday.com и CSharpToday.com. Он также ведет общественный сайт http://www.dotnetforce.com—первый индийский сайт, предоставляющий ресурсы по платформе Mobile.NET. В свободное время любит не спеша погулять с друзьями по берегу моря. С Винодом можно контактировать по адресу: vinod@dotnetforce.com. Эту книгу я хотел бы посвятить Шириди Сатья Сай Баба (Shiridi Sathya Sai Baba), по- скольку он всегда благословлял меня во всех моих начинаниях. Я благодарен Wrox Press за предоставленную мне возможность участвовать в создании этой книги, особенно-Шарлотте и Джулиану за их помощь. Огромное спасибо Шарлотте за постоянную поддержку и терпеливое отношение ко мне с самого первого дня сотрудничества с ASPToday. сот. Номан Аагари Номан Лагари — руководитель группы программистов в компании Creative Cha- os (pvt) Limited в Карачи, Пакистан. У него большой опыт программирования на C/C++, в разработке таких современных проектов, как реализация торговой систе- мы реального времени, соединенной с Electronic Crossing Networks (ECN) и исполь- зующей протокол Financial Information eXchange (FIX) для Wall Street Brokerage. Когда Номан не занят проектированием системной архитектуры, он любит писать научные доклады и статьи, посвященные новым направлениям, особенно использу- ющим платформы Microsoft. Номан жадно читает книги, а поддерживать свою фи- зическую форму предпочитает, играя в крикет. Аджит Мунгале В последние три года Аджит Мунгале является старшим разработчиком про- граммного обеспечения в IBM GSI. За те шесть лет, которые он отдал программиро- ванию, Аджит приобрел опыт работы с самыми разнообразными технологиями. Он начинал с программирования интегральных схем CPLD и FPGA и разработки драй- веров устройств, работал почти со всеми языками и технологиями Microsoft и с про- дуктами IBM. Он специализируется в COM/DCOM/MTS с использованием
xii Об авторах ATL/VB и является экспертом в VB, ASP, C++, XML, IBM MQ Series и .NET Frame- work. Совсем недавно Аджит подал заявку на выдачу патента по шифрованию и Web-безопасности. Аджит любит природу, обладает коллекцией кактусов, карликовых деревьев и других растений. С ним можно пообщаться через почтовый ящик ajit_mungale@hotmail.com. Я хотел бы посвятить эту книгу моим родителям, которые были для меня источником вдохновения. Особо хочу поблагодарить семью Назар (Nazar) за поддержку, а Джулиана - за постоянную помощь в работе над проектом. Кристиан Нагел Кристиан Нагел — преподаватель и консультант в компании Global Knowledge— самой крупной независимой организации, предоставляющей профессиональную подготовку в области информационных технологий. Кристиан начинал свою рабо- ту в области вычислительной техники с платформ PDP 11 и VAX/VMS, освоив са- мые разные языки и технологии. С платформой .NET и языком C# он работает с июля 2000 г., когда они впервые были официально объявлены. Обладая глубокими знаниями технологий Microsoft — он сертифицирован компанией Microsoft как дип- ломированный преподаватель (Microsoft Certified Trainer (МСТ)), разработчик ре- шений (Solution Developer (MCSD)) и системный инженер (System Engineer (MCSE)), — Кристиан также обучает других программированию и архитектуре рас- пределенных решений. В ИТ-бизнесе у него и другие роли: основал группу пользова- телей платформы .NET в Австрии, является региональным директором MSDN, выступает с докладами на международных конференциях и руководит европейским отделением INTEA (Международная ассоциация групп пользователей платформы .NET). Web-сайт Кристиана вы найдете по адресу: http://christian.nagel.net. Хочу поблагодарить Эйлин Крейн, Стэси Джиард и Эрика Ивинга из Microsoft за поддер- жку, а Кристиана Сейдлера - за сотрудничество в Global Knowledge. Особая благодарность - моей жене Элизабет за любовь и поддержку. Г Тим Паркер Уже 25 лет Тим Паркер — программист, писатель и преподаватель. Он написал более 60 книг и 3,5 тыс. журнальных статей. Тим работает в Web-среде с самого ее возникновения и спроектировал сотни Web-сайтов. В свободное время он занимает- ся подводным плаванием, пилотирует самолет и управляет капризной сетью из 30 компьютеров, установленной в его доме в Оттаве, Канада. Маргарет Френсис, доброму и надежному другу -с благодарностью. Шриниваса Шивакумар Шриниваса Шивакумар — консультант по программному обеспечению, разра- ботчик и писатель. Он специализируется в Web-технологиях и мобильных сре- дствах, использующих решения Microsoft. В настоящее время работает в Чикаго в компании TransTech, LLC. Шриниваса участвовал в написании книг Professional ASP.NET Web Services^ Professional ASP.NET Web Services with VB.NET, ASP.NET Mobile Controls — Tutorial Guide, Early Adopter .NET Compact Framework, Beginning ASP.NET 1.0 with VB.NET, Visual Basic .NET Threading Handbook, Beginning ASP.NET 1.0 with C#.NET, Professional ASP.NET Security и технических статей для ASPToday.com, CSharpToday.com, .NET Developer и т. д. В свободное время любит смотреть тамильские фильмы и слушать тамильские записи (особенно пение Бала- субраманияма (S. Р. Balasubramaniyam).
Введение С етевое программирование является одной из центральных задач при разра- ботке уровня бизнес-приложений — потребность в эффективном и безопасном взаимодействии разных компьютеров, находящихся в одном здании или разбросанных по всему миру, остается основной для успеха многих систем. Со средой .NET Framework приходит новый набор классов для решения задач сетевого обмена. Прочитав книгу, вы станете уверенным сетевым программистом на платформе .NET и будете понимать базовые протоколы. В настоящее время набор протоколов, поддерживаемых классами .NET, ограничен для транспортного уровня протокола- ми TCP и UDP, а на прикладном уровне — протоколами HTTP и SMTP. В этой книге мы не только полностью освещаем соответствующие классы, но и рассматриваем примеры реализации в .NET протоколов прикладного уровня. Таким образом, кни- га будет очень полезна для всякого читателя, нуадающегося в использовании прото- колов, не поддерживаемых в настоящее время в .NET, а также для всех тех, кто хочет овладеть предписанными протоколами. Основные темы книги В главе 1 — введение в некоторые основные сетевые понятия и протоколы. Что бы вам ни было нужно от сетевого программирования—разработка серверных при- ложений, выполняемых как Windows-сервисы, которые предоставляют данные кли- ентам с использованием специализированного протокола, написание клиентских приложений, запрашивающих данные от Web-серверов, создание широковещатель- ных приложений или приложений, функционирующих как почтовые службы, эта глава будет вашим первым портом захода. Мы начинаем с рассмотрения физичес- кой сети и оборудования, применяемого в локальных вычислительных сетях. Затем мы обращаемся к таким вещам, как семиуровневая модель OSI, и рассматриваем со- ответствие набора протоколов TCP/IP уровням OSI. Наконец мы изучаем разнооб- разные протоколы сетевого обмена, Интернета и электронной почты. В главе 2 предоставлены сведения о работе с потоками. Поток — это абстрактное представление последовательного устройства, осуществляющего побайтовое со- хранение и считывание данных. Таким устройством может быть, например, файл,
xiv Professional .NET Network Programming принтер или сетевой сокет. Через эту абстракцию один и тот же процесс может об- ращаться к разным устройствам, и схожий программный код можно использовать, к примеру, для чтения данных из входного файлового или входного сетевого пото- ков. В таком случае программист освобождается от необходимости беспокоиться о физической сути конкретного устройства. В данной главе мы рассматриваем пото- ки в .NE^T — класс Stream — и работаем с конкретным классом FileStream. Здесь же освещается чтение из двоичных и текстовых файлов, запись в них и сериализация (преобразование в последовательную форму) объектов bXML и двоичный формат. ГлаваЗ помогает приступить к сетевому программированию в .NET с использо- ванием классов из пространства имен System.Net. Мы начинаем с обсуждения этих классов — они играют фундаментальную роль во всех последующих главах книги. Точнее говоря, мы узнаем, как работать с URI, IP-адресами и поиском в DNS. Мы увидим, как с помощью классов WebRequest и WebResponse обрабатывать запросы и ответы, после чего приступим к рассмотрению аутентификации, авторизации и разрешений, относящихся к сетевому программированию. Глава 4 посвящена программированию сокетов, в ней рассматривается програм- мирование на низших уровнях для выполнения задач сетевого обмена. Сокет — это один конец дуплексного канала связи между двумя программами, которые выполня- ются в сети. Мы рассмотрим поддержку сокетов в .NET — класс System.Net.Soc- kets.Socket и создадим синхронное и асинхронное приложения “клиент-сервер”. В главе 5 мы отправимся в путешествие по высокоуровневым сетевым классам в .NET Framework и начнем с рассмотрения Transmission Control Protocol (TCP). Представим общее введение в TCP, его архитектуру и структуры данных, а затем пе- рейдем к исследованию классов TcpCIient и TcpListener, позволяющих работать с TCP. С помощью этих классов мы строим приложения “клиент-сервер”, полнофун- кциональный почтовый клиент, демонстрирующий мощь TcpCIient, и создаем рабо- тающий по принципу эха многопоточный сервер, опираясь на поддержку классов многопоточной обработки .NET. В заключение главы мы вкратце рассматриваем .NET Remoting Framework и, в частности, транспортный канал TcpChannel, обеспе- чиваемый средой .NET Framework. В главе 6 рассказывается о классе UdpCIient, с помощью которого реализуется User Datagram Protocol (UDP). Рассматриваются основы протокола UDP и исполь- зование класса UdpCIient. Хотя протокол TCP надежнее, чем UDP, он значительно увеличивает накладные расходы. Соответственно, UDP работает быстрее и хорошо приспособлен для передачи таких мультимедийных данных, как потоки видеоизоб- ражения, в которых точный порядок прибытия пакетов несущественен. В этой гла- ве также рассматриваются высокоуровневые протоколы, основанные на UDP. Глава 7 посвящена групповой рассылке. Например, благодаря этой гехнологии в 1994 г. стала возможной трансляция в прямом эфире по Интернету концерта груп- пы “Rolling Stones”. Она позволяет наблюдать за работой космонавтов в космосе или проводить совещания по Интернету. При использовании групповой рассылки сервер должен послать каждое сообщение лишь один раз, и оно распространяется среди целой группы клиентов. Начинается глава со сравнения однонаправленной передачи, групповой рассылки и широковещательной передачи, затем рассматри- вается архитектура групповой рассылки и реализация сокетов групповой рассылки в .NET. С использованием средств групповой рассылки создаются два приложения Windows. Первое приложение позволяет вести переписку между несколькими сис- темами, каждая из которых является и отправителем, и получателем. Второе прило- жение демонстрирует изображения, оно показывает, как объемные пакеты данных можно посылать нескольким клиентам, не занимая значительную часть пропускной способности сети.
Введение xv t-.w,»»» «|ТЩЙ»Х w> Ч-МГ,1^»»<ГМ>у WK W f Д МЧЦ1.1ГЛ1 U4'l|l' ВДЦ****™# W^f. в В главе 8 освещается протокол HTTP и его надежная реализация, предлагаемая в .NET. Важность HTTP как прикладного протокола велика, поскольку в настоящее время он используется в значительной доле Web-трафика. Эта глава начинается с обзора протокола HTTP—заголовков, формата запросов и ответов. Рассматривают- ся классы в .NET, позволяющие работать с HTTP, и рассказывается, как считывать и записывать cookie. Затем с помощью ASP.NET мы создаем HTTP-сервер и, зна- комясь с транспортным каналом HTTP, продолжаем рассмотрение .NET Remoting. В главе 9 мы принимаемся за электронную почту. Начинаем с высокоуровневого обзора разнообразных почтовых протоколов, узнаем, как в среде .NET получить к ним доступ и как ими пользоваться; рассмотрим основы протоколов SMTP, POP3, IMAP и NNTP и увидим, как они совместно работают при отправке и получении электронных сообщений через Интернет. Мы также рассмотрим отправку сообще- ний электронной почты через SMTP с использованием классов .NET Framework и разработку некоторых основных классов, реализующих протоколы POP3 и SMTP. Глава 10 посвящена защите сетевого обмена. Пространство имен System.Securi- ty.Cryptography среды .NET Framework обеспечивает программный доступ к разнообразным сервисам шифрования, которые мы включаем в наши приложе- ния, чтобы шифровать и дешифрировать данные, гарантировать целостность дан- ных и обрабатывать цифровые подписи и сертификаты. В данной главе исследуем это пространство имен и предоставляем введение в криптографию и все ее ключе- вые понятия (за каламбур просим прощения). Мы также рассматриваем обеспече- ние безопасности для приложения форума, созданного в главе 6. Предполагаемый круг читателей Мы освещаем в книге как основные^ так и более сложные сетевые понятия. Чи- татель, знакомый с сетевым программированием в другой среде, сможет довольно быстро освоить содержание этой книги, которая все же будет ему полезна. Все примеры программного кода, приведенные в книге, написаны на С#, поэтому предполагается практическое владение читатель этик языком. Что необходимо для работы с книгой ’ Чтобы запускать примеры, приведенные-в книге, вам требуется машина с уста- новленной средой .NET Framework. Это значит, что на ней должна работать одна из следующих операционных систем: □ Windows 2000 Professional (или более высокий уровень) □ Windows ХР Также для этой книги рекомендуется использовать версию Visual Studio .NET. Соглашения о стилях Мы использовали несколько разных стилей текста и разное расположение в книге, чтобы провести различия между типами информации. Здесь приведены примеры использованных стилей и объясняется их значение. Программный код записывается несколькими шрифтами. Если это слово, о ко- тором идет речь в тексте, например, при обсуждении цикла for (...), то оно записы-
XVI Professional .NET Network Programming вается таким шрифтом. Если это блок кода, который можно ввести как программу и запустить на выполнение, то он записывается на сером фоне. IPHostEntry ipHost = i)ns.Resolve(“127.-0r.O.r->-/ • ". “W ' Иногда можно увидеть код в смешанном стиле, например, так: IPHostEntry ipHost = Dns,ResolVe(“127.0.0.1"); IPAddress ipAddr = ipHost.AddressList[OJ; IPEndPoint ipEndPolnt = new IPEndPoint(ipAddr, 11002); r Socket sender = new Socket(AddressFamily InterNetwork, •; . . ‘ -a . SocketType.Stream, ProtocQlType.Tcp)*' iL. йД Sender.Connect(ipEndPoint); В подобных случаях на белом фоне помещается код, с которым мы уже хорошо знакомы. Строка, выделенная серым фоном, содержит дополнение к рассмотренно- му ранее коду. \Совет, рекомендация и вводная информация представляются таким типом шрифта. ---~--.-V ------------------------ ------m- . Важная информация выделяется такими прямоугольниками. Г 7 Буллиты (пули) показываются с отступами, каждый буллит отмечает новый пункт перечисления следующим образом: □ Важные слова выделяются полужирным шрифтом О Слова, появляющиеся на экране или в меню, например Open или Close, пред- ставлены таким же шрифтом, какой вы могли видеть на рабочем столе Win- dows □ Клавиши, которые вы нажимаете на клавиатуре, например Ctrl и Enter, пред- ставляются курсивом Как скопировать код примеров для этой книги Войдя на Web-сайт Wrox www.wrox.com, найдите название книги с помощью на- шего средства Search или используйте один из списков названий. Затем щелкните по ссылке Download Code на странице с подробной информацией о книге или по элементу Download в столбце Code списка названий. Файлы, доступные для копирования с нашего сайта, архивированы программой WinZip. Когда вы сохраните архивы в какой-либо папке на вашем жестком диске, вам потребуется извлечь файлы из архива с использованием программы распаковки WinZip или PKUnzip. При извлечении файлов код обычно помещается в папки по главам. Начиная процесс извлечения, позаботьтесь, чтобы для вашей утилиты рас- паковки была установлена возможность использования имен папок.
ГЛАВА 1 Сетевые понятия и протоколы р этой главе представлены некоторые основные сетевые понятия и протоко- лы. Она послужит фундаментом, который позволит в остальной части книги за- ниматься сетевым программированием. Не имеет значения, планируете ли вы разрабатывать серверные приложения, выполняющиеся как Windows-сервисы и предоставляющие клиентам данные с использованием специализированного про- токола, или будете писать клиентские приложения, запрашивающие данные от Web-серверов, будете ли вы создавать приложения групповой рассылки или почто- вой службы — в любом случае нужно начать с чтения этой главы. Если вы нетвердо знаете, что представляет собой маршрутизатор или сетевой коммутатор, если не уверены в понимании функциональности семи уровней протокола OSI или просто хотите получить общее представление о различных сетевых протоколах и их использовании или освежить эту информацию в памяти, тогда эта глава — для вас. Мы начнем с представления такого оборудования, используемого в локальных сетях, как маршрутизаторы, концентраторы или хабы и мосты. Затем рассмотрим семь уровней модели OSI и их функциональность и узнаем, как стек протоколов TCP/IP соответствует уровням OSI. После этого мы познакомимся в функциониро- ванием разнообразных сетевых протоколов. В частности, рассмотрим: □ Физическую сеть □ Семиуровневую модель OSI О Основные сетевые протоколы □ Протоколы сети Интернет □ Протоколы электронной почты □ Сокеты □ Поиск имен □ Интернет
Глава 1 □ Удаленную обработку □ Передачу сообщений Физическая сеть По существу сеть представляет собой группу компьютеров или устройств, сое- диненных каналами связи (communication links). В терминах сетевой обработки все компьютеры и устройства (принтеры, маршрутизаторы, коммутаторы и т. д.), присоединенные к сети, называются узлами (nodes). Узлы соединены каналами, ко- торые могут быть кабелями или беспроводными соединениями (инфракрасными или радиосигналами) и могут взаимодействовать с другими узлами, передавая сооб- щения через сеть. Сети различаются в зависимости от размера: □ Локальная сеть (LAN, или Local Area Network) соединяетузлы, расположен- ные на ограниченном участке. Этот участок может соответствовать располо- жению крупной компании или быть совсем небольшим, на котором объединяются компьютеры в жилой квартире. Наиболее часто используемая технология локальных сетей — сеть Ethernet (см. следующий раздел). □ Глобальная сеть (WAN, или Wide Area Network) может объединять несколь- ко участков локальных сетей. Известны такие технологии глобальных сетей, как Frame Relays (с ретрансляцией кадров), линии Tl, Integrated Services Digital Network, или ISDN (цифровая сеть с интегрированными услугами), Х.25 и Asynchronous Transfer Monitor, или ATM (асинхронный режим переда- чи). Далее мы подробнее рассмотрим средства соединения с глобальной сетью. □ Региональная сеть (MAN, или Metropolitan Area Network) очень похожа на глобальную сеть в том, что она тоже объединяет несколько локальных сетей. Однако региональная сеть суживает территорию сети до города или неболь- шого района. В региональной сети используются высокоскоростные сети, со- единяющие локальные сети школ, правительственных учреждений, компа- ний и т. д. с помощью таких быстрых соединений, как волоконная оптика. В разговоре о сетях часто используется термин “магистраль” (backbone). Это высокоскоростная сеть, соединяющая более медленные сети. С помощью магистрали кампания может соединить более медленные сегменты своих локальных сетей. Магистраль Интернета построена на скоростных сетях, передающих трафик глобальной сети. Ваш интернет-провайдер соединен или непосредственно с магистралью Интернета, или с более крупным провайдером, который, в свою очередь, соединен напрямую с магистралью Интернета. Линии глобальной сети Чтобы соединиться с глобальной сетью, имеется несколько возможностей: о Там, где потребителю требуется производительность некоммутируемой се- ти, можно использовать выделенные линии (leased lines). Такие линии обычно оплачиваются по фиксированному тарифу независимо от размеров трафика. Примерами выделенных линий служат Digital Data Service (DDS, работающие на скоростях 2,4 Кбит/сек и 56 Кбит/сек), Т1 (1,544 Мбит/сек) и ТЗ (эквива- лентная 28 линиям Т1). \
Сетевые понятия и протоколы 3 ШММИЧ *’ жюи&гг । ЛЯ*1 UW> JMKMEffp'XiwWiitfff1.')viiW»W»i W.WW ,'Ж1т«nW WilM'WW*111 р*лдиыч^>«ич^’глл-хл-вь«»»ай'-и>я».«<>.-ч» -ч^мчлчичяк><,<• □ Коммутируемые линии (switched lines) применяются в обычной телефон- ной связи. На время вызова или передачи данных между передатчиком и при- емником устанавливается соединение. Когда линия больше не требуется, она освобождается для использования другим потребителем провайдера сети. Примеры коммутируемых сетей — POTS (Plain Old Telephony Service — стан- дартные аналоговые линии, поддерживающие скорости до 56 Кбит/сек), ISDN и DSL (Digital Subscriber Line). □ Сеть с коммутацией пакетов (packet-switching) используется там, где про- вайдер связи предоставляет технологии коммутации с магистральной сетью. Это решение обеспечивает повышенную производительность, разделяя ре- сурсы между потребителями так, чтобы пропускная способность сети была доступна по запросу. Протоколы, используемые для сетей с коммутацией, включают Х.25 (до 64 Кбит/сек), Frame Relay (до 44,736 Мбит/сек) и ATM (до 9,953 Гбит/сек). Протокол Ethernet Чтобы лучше понять, как работают физические сети, мы рассмотрим Ethernet, наиболее распространенный протокол для локальных сетей. Из всех устройств, подключенных к локальным сетям, 90% используют протокол Ethernet, который первоначально в 1972 г. разработали компании Xerox, Digital Equipment и Intel. В1980 г. стандарт IEEE 802.3 CSMA/CD определил протокол Ethernet со скоростью передачи 10 Мбит/сек. В настоящее время сети Ethernet могут поддерживать линии со скоростями 100 Мбит/сек и 1 Гбит/сек. С Ethernet могут применяться многие кабельные тех- нологии. С помощью стандартизованной системы имен указывают скорость сети Ethernet и свойства кабельной технологии. Эти имена начинаются с числа, означа- ющего максимальную скорость передачи данных, за которым следует слово, указы- вающее поддерживаемую технологию передачи, и наконец число, определяющее максимальное расстояние между узлами. Например, 10Base2 обозначает сеть Ethernet, функционирующую со скоростью 10 Мбит/сек, использующую узкополос- ную передачу по кабелям с максимальной длиной 200 м. Вот еще несколько распрос- траненных конфигураций: Стандарт Ethernet Скорость Обычный тип кабеля Описание 10Base5 10 Мбит/сек Коаксиальный медный Это первоначальный стандарт Ethernet, так называемая технология толстого кабеля. WBaseT 10 Мбит/сек Медный 10BaseT — это сеть со скоростью 10 Мбит/сек и кабелем в виде витой пары. Витая пара — это действительно пара проводов, свитых вместе WOBaseTX 100 Мбит/сек Медный Сеть со скоростью 100 Мбит/сек, кабелем в виде витой пары и возможностью полного дуплекса (X) Полный дуплекс означает, что данные могут передаватьсй одновременно в обоих направлениях. WOOBaseSX 1000 Мбит/сек Многомодовое стекловолокно Сеть со скоростью 1000 Мбит/сек и волоконно-оптическими кабелями. Буква S означает короткую (850 нм) длину волны лазера.
4 Глава 1 CSMA/CD Интернет — это сеть CSMA/CD (множественный доступ с контролем несущей и обнаружением конфликтов (коллизий)). Несколько устройств подсоединены к од- ной сети, и все они могут обращаться к ней одновременно. Когда посылается со- общение, оно передается по всей сети, как показано на следующем рисунке. Получатель определяется уникальным адресом, и только этот узел читает сообщение, остальные его игнорируют. В такой схеме заложена потенциальная проблема, поскольку одновременно мо- гут попытаться отправить сообщение не один, а несколько узлов, что приводит к ис- кажению пакетов. Используемое в Ethernet решение состоит в том, что каждый узел прослушивает сеть и, таким образом, знает, есть ли в данный момент трафик в сети. Узел может начать отправку данных, если по сети еще не посланы другие данные. Короче говоря, это часть CSMA (множественный доступ с контролем несущей) протокола CSMA/CD. Однако по-прежнему остается возможность, что два узла, проверив, что сеть пока не используется, начнут отправку пакета точно в одно время по одному сетево- му кабелю. Это приведет к возникновению конфликтов между двумя пакетами, и в результате данные будут искажены. Оба отправителя узнают об искажении паке- та, поскольку при посылке данных они продолжают слушать сеть и обнаруживают конфликт. В протоколе CSMA/CD это часть CD (обнаружение конфликтов). Тогда оба узла немедленно останавливают передачу и ждут в течение случайного про- межутка времени, по истечении которого они снова проверяют состояние сети, чтобы выяснить, свободна ли она и можно ли отправить пакет повторно. Для уникальной идентификации каждый узел локальной сети использует адрес управления доступом к среде (Media Access Control, МАС). Этот адрес определен се- тевой интерфейсной платой. Сетевой пакет отправляется по сети, и, если сетевая плата определяет, что пакет не предназначен для ее хоста, она игнорирует пакет и передает его дальше. Если же пакет предназначен для этой платы, она все равно передает его дальше, но на этот раз отмечает в нем, что он получен. Пакет продол- жает свой путь по сети, пока не вернется к отправителю, который убеждается, что намеченный адресат получил данные. * Другие протоколы Компания IBM разработала сеть Token Ring (IEEE 802.5), узлы которой соедине- ны в кольцо, как видно на следующем рисунке. В технологии Ethernet любой узел мо- жет отправить сообщение, если в сети нет трафика. В сети Token Ring каждый узел имеет гарантированный доступ к сети в предопределенном порядке. По кольцу сети циркулирует маркер, и только узел, владеющий маркером, может отправить сооб-
Сетевые понятия и протоколы 5 щение. В настоящее время Ethernet постепенно вытесняет технологию Token Ring как более дорогую и сложную в реализации. AppleTalk — это протокол локальной сети, разработанный компанией Apple, был очень популярным в школах, на предприятиях и т. д. Асинхронный режим передачи (Asynchronous Transfer Mode, ATM) — еще один протокол, который можно встретить в локальных сетях. Он поддерживает быструю коммутацию сети и имеет гарантированное “качество обслуживания” (Quality of Service, QoS), но, поскольку цена сетевых плат ATM очень высока, ATM применяет- ся лишь в узкоспециализированном секторе рынка локальных сетей. ATM использу- ется лишь для сетей LAN, требующих чрезвычайно высокой производительности, например, для передачи между больницами таких медицинских изображений, как рентгеновские снимки. В магистрали, управляющей глобальными сетями, ATM иг- рает более важную роль. Физические компоненты Для понимания сетей важно знать компоненты оборудования. Мы собираемся рассмотреть основные компоненты локальной сети: □ Сетевая интерфейсная плата □ Концентратор □ Коммутатор □ Маршрутизатор Сетевая интерфейсная плата Сетевая интерфейсная плата (Network Interface Card, или NIC) — это адаптерная плата, используемая для соединения устройства с локальной сетью. Она позволяет отправлять сообщения в сеть и получать сообщения из сети. Сетевая плата имеет уникальный МАС-адрес, обеспечивающий уникальную идейтификацию для каждо- го устройства. МАС-адрес — это 12-байтное шестнадцатеричное число, уникально назначенное сетевой Ethernet-плате. Этот адрес может быть изменен сетевым драйвером дина- мически (как, например, в сетях DECnet, разработанной компанией Digital Equip- ment), но обычно МАС-адрес не изменяется.
Глава! Вы можете узнать МАС-адрес машины Windows с помощью утилиты командной строки ipconfig, запустив ее в окне DOS с установленным переключателем /all. На следующем снимке экрана показан вывод, полученный на системе, в которой МАС-адрес равен 00-50-DA-E2-2C-97. Первая часть этого числа, 00-50-DA, назначена изготовителю сетевой платы. Оставшуюся часть числа изготовитель использует, чтобы создать уникальный МАС-адрес: Концентратор (хаб) Несколько устройств можно без труда связать с помощью концентратора (хаба, hub) — устройства, соединяющего с локальной сетью несколько устройств. Обычно каждое устройство подсоединяется к порту концентратора через неэкранирован- ную витую пару (Unshielded Twisted Pair, или UTP). Вы уже слышали о соединителе RJ-45 (Registered Jack-45). Это один из возможных типов порта в концентраторе, ко- торый может поддерживать и другие типы кабелей. Концентратор имеет л^рбое число портов от 4 до 24. В крупной сети несколько концентраторов устанавли- ваются в одном шкафу и поддерживают сотни соединений. Концентратор действует как повторитель, передавая сообщение, поступившее на один порт, на все другие порты. Это довольно простой элемент, функционирую- щий на физическом уровне сети и ретранслирующий данные без какой-либо обра- ботки. Поэтому концентраторы легко устанавливать и ими легко управлять, ведь они не требуют никакого конфигурирования. Коммутатор Коммутаторы (switches) разделяют сети на сегменты. По сравнению с концентра- тором коммутатор—устройство более интеллектуальное. Коммутатор хранит подсое-
Сетевью понятия и протоколы 7 диненные к его портам МАС-адреса в таблицах преобразования. С помощью этих таблиц коммутатор фильтрует сообщения из сети и в отличие от концентратора не допускает пересылки сообщений к каждому порту. Тем самым исключаются возмож- йые конфликты и повышается производительность сети. Коммутация выполняется на аппаратном уровне (через специализированные интегральные схемы). Как видно на следующем рисунке, коммутаторы используются для соединения концентраторов локальной сети. Если узел А посылает сообщение узлу В, коммута- тор не направляет сообщение сегменту 2, поскольку знает, что узел В находится в той же части сети, что и узел А. Однако, если узел А отправляет сообщение узлу С, оно передается от сегмента 1 к сегменту 2. Этот вид расположения пользовался популярностью раньше, когда концент- раторы были гораздо дешевле коммутаторов, но теперь его распространение со- кращается, поскольку цены коммутаторов упали и почти сравнялись с ценами на концентраторы. Из-за повышения производительности сети, связанной с сокраще- нием конфликтов, в новых сетях коммутаторы часто используются вместо концент- раторов, и оконечные пользователи подсоединяются напрямую к коммутаторам. Маршрутизатор Маршрутизатор (router) — это промежуточное сетевое устройство, соединяю- щее несколько физических сетей. Если хост-машин много, иногда полезно разбить локальную сеть на отдельные части, или подсети. Подсети приносят следующие преимущества: □ Производительность повышается из-за сокращения потоков широковеща- тельной передачи (broadcasts), при которой сообщение рассылается всем узлам сети. Если определены подсети, сообщение отправляется только узлам соответствующей подсети. □ Возможность ограничить пользователей конкретными подсетями дает до- полнительную безопасность. □ Меньшими подсетями легче управлять, чем одной крупной сетью. □ Подсети позволяют распределить одну сеть по нескольким местам. На следующей схеме показано, как маршрутизаторы соединяют несколько под- сетей.
8 Глава 1 Используя в локальной сети маршрутизатор, имейте в виду, что он работает не так быстро, как коммутатор. При получении сообщения он выполняет более значительную обработку, чем коммутатор, и соответственно тратит немного больше времени, прежде чем пересылает пакеты дальше. Маршрутизаторы в глобальных сетях также занимают важное место, соединяя различные линии. Маршрутизатор получает сообщение и направляет его адресату, используя последний известный ему наилучший путь к этому адресату: Маршрутизатор хранит таблицу маршрутов с перечнем путей, по которым мож- но попасть в конкретные сети. Часто из одной сети можно попасть в другую по нес- кольким маршрутам, но всегда один из них будет наилучшим, и в таблице маршрутов описан именно этот путь. Маршрутизаторы взаимодействуют через протоколы
Сетевые понятия и протоколы 9 маршрутизации, обнаруживающие другие маршрутизаторы в сети и поддерживаю- щие обмен информацией о сетях, присоединенных к каждому маршрутизатору. Информацию, которую собирает маршрутизатор о путях между сетями, называ- ют метрикой маршрутизатора (router metrics), в нее включаются такие данные, как потери пакетов и время передачи. Используемая в метрике информация зависит от протокола маршрутизации: п Протоколы маршрутизации по вектору расстояния Протокол маршрутной информации (Routing Information Protocol, RIP) и протокол внутренней маршрутизации между шлюзами (Interior Gateway Routing Protocol, IGRP) используют счетчик прыжков (хопов) (hop count), указывающий число маршрутизаторов, которые необходимо пройти по пути в целевую сеть. В этих протоколах предпочтение отдается путям С меньшим числом маршрутизаторов независимо от скорости и надежности. □ Протоколы маршрутизации по состоянию канала При вычислении наилучшего пути протокол с первоочередным открытием кратчайшего пути (Open Shortest Path First, OSPF) и пограничный межсете- вой протокол (Border Gateway Protocol, BGP) учитывают несколько факто- ров: скорость, надежность и даже стоимость пути. □ Гибридные протоколы маршрутизации Гибридные протоколы при вычислении комбинируют вектор расстояния и состояние канала. Определение маршрута При конфигурировании TCP/IP можно установить шлюз по умолчанию (de- fault gateway). Для этого нужно задать IP-адрес порта маршрутизатора, к которому подключена подсеть машины. Этот маршрутизатор используется, когда требуется соединение с хостом за пределами подсети. Местную таблицу маршрутов можно увидеть в Windows, если ввести ROUTE PRINT из командной строки. Эта команда отображает шлюзы, которые будут использо- ваться для соединения с каждой сетью. На следующем рисунке показан вывод для машины с IP-адресом 192.168.0.1 с двумя сетевыми интерфейсами (одной сетевой платой локальной сети и одним соединением с глобальной сетью). При обращении к хосту с адресом 192.168.0.x в качестве шлюза используется местный IP-адрес 192.168.0.1—с этими хостами мы можем соединяться напрямую. Для других сетевых назначений используется маршрутизатор 212183 100.220: I
10 Глава 1 У команды ROUTE есть опция (ROUTE ADD), чтобы задать IP-адрес маршрутизатора (шлюза) и сетевой адрес, который будет использоваться с этим маршрутизатором. Тогда этот маршрутизатор будет использоваться для соединения с хостами в ука- занной сети. Стоит рассмотреть еще одну полезную команду — TRACERT. Она позволяет иссле- довать путь, который используется для достижения пункта назначения. Просто ука- жите после команды TRACERT имя хоста или IP-адрес — в приведенном примере TRACERT www.globalknowledge.com — и увидите перечень всех маршрутизаторов, используемых, чтобы попасть к указанному хосту. Как можно видеть на следующем снимке экрана, эта команда также выводит время, необходимое для следующего прыжка. Команда TRACERT очень полезна, если с хостом нельзя связаться; это мо- жет означать, что некоторая сеть между вашей машиной и адресатом не работает или недоступна. Многоуровневая модель OSI В целях формализации процесса взаимодействия открытых систем OSI (Open System Interconnection) Международная организация по стандартизаций (Interna- tional Organization for Standardization, ISO) разработала для стандартизированной сети модель, которая заменила бы TCP/IT, DECNet и другие протоколы как основ- ной сетевой протокол, использующийся в Интернете. Однако из-за сложности про- токола OSI созданы и внедрены в эксплуатацию лишь немногие реализации. TCP/IP был гораздо проще, и поэтому он широко используется и сейчас. Но многие новые идеи протокола OSI можно обнаружить в IPv6, следующей версии IP. Хотя протокол OSI не получил широкого распространения, семиуровневая мо- дель OSI имела огромный, успех, и она теперь используется как справочная модель, чтобы описать различные сетевые протоколы и их функциональные возможности. Уровни модели OSI выделяют основные задачи, которые должны выполняться сетевыми протоколами, и описывают взаимодействие сетевых приложений. У каж-
Сетевые понятия и протоколы 11 дого уровня есть особое назначение, и каждый уровень связан с уровнями, находя- щимися непосредственно выше и ниже его. OSI определяет следующие семь уровней: Прикладной Представительский Сеансовый Транспортный Сетевой Канальный Физический □ Прикладной уровень определяет для пользовательских приложений про- граммный интерфейс с сетью. □ Представительский уровень отвечает за кодирование данных, полученных от прикладного уровня, в представление, готовое к передаче по сети, и наобо- рот. □ Сеансовый уровень создает виртуальное соединение между приложениями. □ Транспортный уровень делает возможным надежный обмен данными. □ Сетевой уровень позволяет обращаться к узлам локальной сети, используя ло- гическую адресацию. □ Канальный уровень получает доступ к физической сети через физические ад- реса. □ Физический уровень включает соединители, кабели и т. д. На следующем рисунке показана связь между двумя машинами. Здесь можно уви- деть, как данные опускаются по стеку протоколов (protocol stack) на отправителе и поднимаются по нему на получателе. Сообщение, отправленное приложением первой машины, показано на рисунке клеткой, содержащей букву D. Прикладной уровень (уровень 7) присоединяет к сообщению заголовок (названный на рисун- ке Н7) и передает сообщение представительскому уровню (уровень 6), который сначала добавляет к сообщению Н6, а затем передает его сеансовому уровню (уро- вень 5). Это продолжается до тех пор, пока сообщение со всеми его заголовками не достигнет физического уровня (уровень 1) и не будет передано получателю. На по- лучающей стороне каждый уровень выполняет всю необходимую обработку и, уда- лив соответствующий заголовок, передает сообщение вверх на следующий уровень. В конце концов приложение-получатель получает доступ к исходным данным, отправленным приложением первого компьютера: Теперь, когда нам стала понятна концепция семи уровней, мы можем рассмот- реть функциональность каждого уровня более подробно. Начнем с самого низа и бу- дем продвигаться вверх.
12 Глава 1 ф- '' ,' ' aft'' '.<• Отправитель ^5 S’ t * 1 e' _______ _ 1'•***"• Получатель .лаЛ fe 7 6 7 6 .J» ‘ ...... v- IH6:H7IDI ; H5~, Нб| H71 p| |Н4'Н5|Н6ГН7|Р1 I--' , 5 4 5 }Н5 Н6|Н7|Р' 4 ><< (Н4|Н5|Н6|Н7|Р| ' - 3 3 |H2jH3 jH4jH5|Нб|H7| D~| J < ; IH21H31H41H51H61H7 ID I' - .—1— —yL_ 2 1 2 i 1 ‘ ' * «g. * ‘ •O, 1Н2;Н3|Н41Н5|Н61Н7| DI L Я Передача по сетевым кабелям Ш , .. j □ Уровень 1: физический уровень Физический уровень содержит физическую среду, в том числе требования к ка- белям, соединители, спецификации интерфейсов, спецификации концентраторов, повторителей и т. д. На этом уровне точно определяется, какой физический сигнал будет использоваться для посылки “1", а какой будет представлять ”0". Уровень 2: канальный уровень МАС-адрес, о котором уже рассказывалось, относится к уровню 2. Узлы локаль- ной сети отправляют друг другу сообщения, используя IP-адреса, а они должны транслироваться в соответствующие МАС-адреса канальным уровнем. Протокол разрешения адресов (Address Resolution Protocol, ARP) трансли- рует IP-адреса в МАС-адреса. Кэш, содержащий известные МАС-адреса, ускоряет этот процесс, и его можно исследовать с помощью утилиты агр. Команда а гр -а по- казывает МАС-адреса всех недавно использованных узлов в кэше ARP:
Сетевые понятия и протоколы 13 Утилита агр также позволяет установить соответствие между IP-адресами и MAP-адресами, с тем чтобы ARP-запросы для МАС-адресов больше не требова- лись. Однако это соответствие будет нарушено при замене сетевой платы, поэтому им следует пользоваться осторожно. Другие обязанности канального уровня состоят в отправке и получении сообще- ний и обнаружении ошибок. В Ethernet также требуется обнаруживать конфликты, что мы уже обсуждали. Коммутатор сети действует на канальном уровне, фильтруя сообщения в соотве- тствии с МАС-адресами их получателей. Уровень 3: сетевой уровень Уровнем выше над канальным уровнем находится сетевой уровень. На уровне 3 для соединения с другими узлами используется логическая адресация- МАС-адреса уровня 2 могут использоваться только внутри локальной сети, а обращаясь к узлам глобальной сети, надо использовать адресацию уровня 3. Internet Protocol (IP) — это протокол уровня 3; для идентификации узлов сети он использует IP-адреса. Маршрутизаторы выполняют маршрутизацию трафика между сетями на уров- не 8. Уровень 4: транспортный уровень На сетевом уровне хосты идентифицируются логическими адресами. На транс- портном уровне идентифицируется приложение через так называемую конечную точку (endpoint). В протоколе TCP конечная точка задается комбинацией номера порта (port number) и IP-адреса. Транспортный уровень различается в зависимости от того, используем ли мы надежную или ненадежную связь. При надежной связи, если сообщение было от- правлено, но не было корректно получено, вырабатывается ошибка, в то время как при ненадежной связи сообщение отправляется, но никакой проверки, было ли оно вообще получено, не выполняется. При надежной связи транспортный уровень от- вечает за отправку подтверждений на пакеты данных, что позволяет повторно пере- давать сообщения в случае искажения или потери данных, браковать дублирующие сообщения и т. д. Сетевая связь на транспортном уровне может также различаться в зависимости от того, ориентирована ли она на соединения, или соединения отсутствуют: □ При связи, ориентированной на соединения, до отправки или получения со- общения требуется установить соединение. □ При связи без соединений устанавливать отдельные соединения необязатель- но и сообщения отправляются немедленно. Протокол TCP использует ориентированный на соединения механизм связи, в то время как в UDP (User Datagram Protocol) используется механизм связи без орга- низации соединений. Ориентированная на соединения связь является надежной, поскольку в этом случае отправляются подтверждения и сообщения посылаются по- вторно, если данные не получены или по какой-либо причине они были искажены. Связь без установления соединений может быть полезна при широковещательной передаче, когда сообщения отправляются нескольким узлам. В этом случае доставка сообщения не гарантируется. Если необходим надежный обмен сообщениями, надежность может обеспечить протокол более высокого уровня, подключенный поверх механизма без установления соединения.
14 Глава 1 Уровень 5: сеансовый уровень В модели OSI сеансовый уровень определяет обслуживание приложения, напри- мер, вход в приложение и выход из приложения. Сеанс представляет собой вирту- альное (логическое) соединение между приложениями. Соединение сеансового уровня не зависит от расположенного ниже физического соединения на транспор- тном уровне, виртуальное соединение может существовать дольше, чем соединение на транспортном уровне. Для одного соединения сеансового уровня может потре- боваться несколько соединений транспортного уровня. Эти функциональные возможности мы сравниваем с возможностями, которые предоставляют сеансовые объекты ASP.NET. Сеансовые объекты существуют, пока сеанс не закончится по тайм-ауту (обычно через 20 минут), независимо от ТСР-сое- динений более низкого уровня. Уровень 6: представительский уровень Представительский уровень используется для форматирования данных в соот- ветствии с требованиями приложений. На этом уровне обычно выполняются шиф- рование, дешифрование и сжатие. Уровень 7: прикладной уровень Прикладной уровень—самый верхний уровень модели OSI. Этот уровень содер- жит приложения, в которых используются сетевые средства. Приложения могут выполнять такие задачи, как передача файлов, печать, электронная почта, Web- браузинг и т. д. Учебные приложения, которые мы будем создавать в этой книге, принадлежат этому уровню. Сетевые протоколы Уровни OSI определяют модель уровней протоколов, их назначение и совмес- тную работу. Сравним уровни OSI с конкретной реализацией — стеком протоколов TCP/IP. Стек протоколов TCP/IP—это простая форма модели OSI, которую можно представить не семью, а четырьмя уровнями. Протокол IP соответствует уровню 3 OSI, TCP и UDP—уровню 4 OSI. HTTP, FIT и SMTP не помещаются в одном уровне модели OSI, и задачи, которые они выполняют, относятся к сеансовому, представи- тельскому и прикладному уровням.
Сетевые понятия и протоколы 15 В следующем разделе мы рассмотрим функциональные возможности и назначе- ние протоколов набора TCP/IP в таком порядке: □ Базовые протоколы □ Протоколы Интернета О Протоколы электронной почты □ Другие протоколы Базовые протоколы Как можно видеть, набор протоколов TCP/IP разделен на уровни гораздо про- ще, чем предусмотрено моделью OSI. TCP и UDP — это транспортные протоколы, соответствующие уровню 4 OSI. Они используют IP, протокол уровня 3 OSI (сетево- го уровня). Кроме этих трех протоколов, в наборе протоколов TCP/IP есть еще два базовых протокола, расширяющих IP: ICMP и IGMP. Функциональные возможнос- ти этих протоколов должны быть реализованы в уровне, содержащем IP, поэтому на предыдущем рисунке они показаны на том же уровне. \ IP—Internet Protocol Internet Protocol соединяет два узла. Каждый узел идентифицируется 32-битным адресом, называемым IP-адресом. При отправке сообщения IP-протокол получает его от протоколов верхнего уровня, TCP или UDP, и добавляет IP-заголовок, содер- жащий информацию о хосте-адресате. Чтобы понять протокол IP, самый лучший способ — детально исследовать IP-за- головок. Содержащаяся в нем информация приведена в таблице: Поле Длина Описание Версия IP 4 бита Версия протокола IP. создавшего заголовок. Текущая версия протокола IP —4. Длина IP-заголовка 4 бита Длина заголовка. Минимальное значение — 5, в единицах по 32 бита, или 4 байта. Следовательно, минимальная длина заголовка равна 20 байтам. Тип обслуживания 1 байт Поле типа обслуживания позволяет отправлять сообщения с нормальной или высокой производительностью, нормальной или увеличенной задержкой, нормальной или высокой надежностью. Это поле полезно при отправке в сеть дейтаграмм. Несколько разновидностей сетей используют эту информацию, чтобы выделить приоритет определенного трафика. - - Кроме того, сообщения управления сетью по сравнению с обыч- ными сообщениями имеют повышенные приоритет и надежность. Общая длина 2 байта В этих двух байтах задается общая длина сообщения — заголовка и данных— в октетах. Максимальный размер IP-пакета равен 65 535 байтов, но для большинства сетей такой размер непрактичен. Самый большой размер, который может быть принят всеми хостами, равен 576 байтам. Длинные сообщения могут разделяться на фрагменты—такой процесс называется фрагментацией. Идентификация' 2 байта Если сообщение разбито на фрагменты, поле идентификации помогает собрать фрагменты сообщения. Все фрагменты одного сообщения имеют один и тот же идентификационный номер.
Глоба 1 w««M*^*mi»*w*w*w*iW** 16 продолжение таблицы Флаги Збита Эти флаги указывают, фрагментировано ли сообщение и является ли текущий пакет последним фрагментом сообщения. Смещение фрагмента 13 битов В этих 13 битах задается смещение фрагментированного сообще- ния. Фрагменты могут поступать не в том порядке, в каком они были отправлены, поэтому смещение необходимо, чтобы восстано- вить исходные данные. Первый фрагмент сообщения имеет длину 0, а в остальных фрагментах дается смещение, по которому следует поместить фрагмент. Единица смещения равна 8 байтам, так что значение смещения 64 означает, что второй фрагмент нуж- но присоединить к сообщению после 512 байтов первого пакета. Время жизни 1 байт Значение “время жизни" (TTL) задает число секунд, которое сообщение может существовать, прежде чем будет отброшено. В этом значении необязательно указывается число секунд, поскольку каждый маршрутизатор, пересекаемый сообщением, должен уменьшить значение TTL на 1, даже если он затратил на обработку сообщения меньше одной секунды. Поэтому на практике в этом значении задается число допустимых “прыжков". Протокол 1 байт В этом байте указывается протокол, используемый на следующем уровне стека протоколов для этого сообщения. Номера протоколов определены в доступной оперативной базе данных Internet Assigned Number Authority (IANA): http://www.iana.org/assignments/protocol-numbers. - - К примеру: ICMP имеет значение 1, IGMP — 2, TCP — 6, UDP —17. Контрольная сумма заголовка 2 байта Это контрольная сумма одного заголовка. Поскольку заголовок изменяется с каждым отправленным сообщением, контрольная сумма также изменяется. Адрес источника 4 байта В этом поле указывается 32-битный IP-адрес отправителя. Адрес назначения 4 байта Это 32-битный IP-адрес, по которому отправлено сообщение. Опции переменная Здесь могут появляться необязательные поля. Например, можно указать, что это сообщение секретно или совершенно секретно. Также предусмотрена возможность будущих расширений. Дополнение переменная \ Это поле содержит переменное число нулей, такое, чтобы заголовок заканчивался на 32-битной границе. Internet Protocol (IP) определен в НРС 791 Документы RFC (Request for Comments) содержат техническую информацию о многих важных интернет-технологиях. Эти документы можно найти на странице http://vvww fetf.org/rfc.html. IP-адреса Каждый узел в сети TCP/IP может быть идентифицирован 32-битным IP-адре- сом. Обычно IP-адрес представляется четырьмя десятичными значениями в таком виде: 192.168.0.1. Каждое из этих чисел представляет собой один байт IP-адреса и может находиться в пределах от 0 до 255. IP-адрес содержит две части: сетевую часть и часть хоста. В зависимости от клас- са сети сетевая часть состоит из одного, двух или трех байтов:
Сетевые понятия и протоколы 17 Класс Байт1 Байт 2 БайтЗ Байт 4 А Сеть (1—126) Хост (0—255) Хост (0—255) Хост (0—255) В Сеть (128—191) Сеть (0—255) Хост (0—255) Хост (0-255) С Сеть (192—223) Сеть (0—255) Сеть (0—255) Хост (0—255) Первый бит адреса сети класса А должен быть 0, поэтому первый байт для сети класса А имеет двоичные значения в пределах от 00000001 (1) до .01111110 (126). Остальные три байта служат для идентификации узлов в сети, позволяя соединить в сети класса А более 16 млн. устройств. Заметим, что в приведенной таблице адреса с числом 127 в первом байте пропу- щены, поскольку это зарезервированный диапазон адресов. Адрес 127.0.0.1 — это всегда адрес локального хоста, а 127.0.0.0 — адрес локальной обратной связи. Об- ратная связь используется для тестирования стека сетевых протоколов на одной машине, без прохода через сетевую интерфейсную плату. В IP-адресе для сети класса В первые два бита всегда имеют значение 10, что дает диапазон от 10000000 (128) до 10111111 (191). Второй байт продолжает идентифи- кацию сети значением от 0 до 255, оставляя два последних байта для идентифика- ции узлов сети, всего до 65 534 устройств. Сети класса С отличаются IP-адресом, в котором в первых трех битах установле- но значение ПО, разрешая значения в диапазоне от 11000000 (192) до 11011111 (223). В сети этого типалишь один байт оставлен для идентификации узлов, поэто- • му к ней можно подсоединить только 254 устройства. Число устройств, которое можно подсоединить к сети каждого из этих классов с особыми IP-адресами, обратно пропорционально числу возможных сетей этого типа. Например, сеть класса А, допуская 16 млн. хостов, оставляет только часть первого байта для идентификации сети. В результате во всем мире может существовать лишь 126 сетей класса А. Только крупные компании, подобные АТ & Т, IBM, Xerox и HP, имеют такой сетевой адрес. Когда компания запрашивает IP-сеть в органе, ведающем сетями, обычно она получает сеть класса С. Если компания пожелает, чтобы больше хостов напрямую были подключены к Интернету, можно найти еще одну сеть класса С. Если для каждого хоста в сети не требуется прямого доступа к Интернету, можно использовать частный 1Р-адрес, и тогда применяется другая опция. Частные IP-адреса мы обсудим в следующем разделе. Сетевые адреса классов А, В и С оставляют свободными адреса, имеющие в пер- вом байте значения от 224 до 255. Как будет показано в главе 7, сети класса D (224—239) используются для групповой рассылки, а класс Е (240—255) резервирует- ся в целях тестирования. Агентство IANA выделяет номера сетей и публикует их перечень на странице http://www.iana.org/assigntnents/ipv4-address-space. Почти во всех странах есть региональные регистрационные ведомства, выдающие по запросам номера , сетей. Региональные ведомства получают диапазон сетей от IANA. Частные 1Р-адреса Чтобы избежать исчерпания IP-адресов, хосты, не соединенные напрямую с Интернетом, могут использовать адреса из диапазонов частных адресов. Частные адреса уникальны не глобально, а только локально, внутри сети. Во всех классах се- тей резервируются определенные диапазоны, которые могут использоваться как
48 Глава 1 частные адреса хостами, не требующими непосредственного двустороннего досту- па к Интернету. Такие хосты вполне могут обращаться к Интернету через шлюз, который не посылает во внешнюю сеть частный IP-адрес. Класс Диапазон частных адресов (сетевая часть IP-адреса) А 10 В 172.16—172.31 С 192.168.0—192.168.255 Выделение частных адресов описано в RFC 1918. Подсети Для соединения двух узлов в разных сетях требуется маршрутизатор. Номер хоста определяется 24 битами IP-адреса класса А, в то время как для сети класса С доступно лишь 8 битов. Маршрутизатор разделяет номер хоста на номер подсети и номер хоста в подсети. Включение дополнительных маршрутизаторов сократит объемы широковещательной передачи в сети, а это может сократить нагрузку в сети. Новые маршрутизаторы главным образом включаются, чтобы улучшить воз- можность соединения между группами компьютеров в разных зданиях, городах и т. д. Рассмотрим пример разделения сети класса С с адресом 194.180.44 на подсети. Такая сеть может фильтровать адреса с помощью маски подсети (subnet mask) 255.255.255.224. Первые три байта (состоящие из всех единиц) представляют собой • маску для сети класса С. Последний байт — это десятичное значение двоичного представления 11100000, в котором первые три бита адреса хоста указывают под- сеть, а последние пять битов представляют адрес хоста в конкретной подсети. Три бита подсети представляют 128, 64 и 32, и, таким образом, поддерживаются адреса подсетей, показанные ниже:
Сетевые понятия и протоколы 19 Итак, подсеть 194.180.44.64 будет содержать хосты с адресами от 194.180.44.65 до 194.180.44.94, а подсеть 194.180.44.160 — от 194.180.44.161 до 194.180.44.190. IPv6 Протокол, предшествовавший Internet Protocol, был разработан Управлением перспективных исследовательских работ Министерства обороны США (DARPA) в 1960-х годах, а набор протоколов TCP/IP получил признание лишь в 1980 г. По- скольку IP базировался на существовавших сетевых протоколах DARPA, он получил номер версии 4 и теперь известен как IPv4. В те времена, когда человечество в боль- шинстве своем представляло себе мобильный телефон как трубку, которую можно снимать со стены и переносить к дивану, число хостов, поддерживаемых IP, каза- лось более чем достаточным. Однако сегодня все хотят подключить к Интернету хо- лодильники и газонокосилки, и IETF разрабатывает новую версию IP — IPv6. Наиболее важное изменение этой версии по сравнению с IPv4 заключается в ис- пользовании для адресации не 32, а 128 битов, что позволит всем Tablet PC, Pocket PC, мобильным телефонам, телевизорам, автомобилям, газонокосилкам, кофевар- кам и мусорным контейнерам стать полноправными хостами Интернета. Кремле возможности назначить адрес почти каждому атому в Солнечной систе- ме, в IPv6 появляется еще несколько полезных изменений: □ Возможности расширенной адресации Чтобы определить диапазон адресов групповой рассылки, в адреса IPv6 мо- жет включаться маршрутная информация о группах. Кроме того, появляется альтернативный адрес для отправки сообщения любому хосту или любой группе хостов. □ Упрощение формата заголовка Некоторые поля заголовка IPv4 удаляются, другие становятся необязательны- ми. Однако полная длина заголовка IPv6 больше, чем в IPv4 из-за 128-битных адресов источника и назначения. □ Улучшенная поддержка расширяемости В будущем добавлять расширения к протоколу IPv6 станет легче. Ограниче- ния на длину для опций удалено. □ Маркирование потока Для конкретных потоков трафика добавляется новая возможность. Поток — Это последовательность пакетов, перемещающаяся от источника к назначе- нию. В новом протоколе приложения могут предлагать аудио- И видеовозмож- ности в реальном времени по различным потокам. Каждый поток может запрашивать обработку в реальном времени или особо качественную обра- ботку у маршрутизаторов, через которые он распространяется. □ Аутентификация и секретность Добавляются расширения IPv6, поддерживающие аутентификацию, секрет- ность и конфиденциальность отправляемых данных. Транспортный уровень—номера портов Для идентификации узлов сети протокол IP использует IP-адреса, а транспорт- ный уровень (уровень 4) использует конечные точки для идентификации приложе- ний. Чтобы указать конечную точку приложения, протоколы TCP и UDP вместе с IP-адресом используют номер порта. -1
20 Главой Сервер должен предоставить известную конечную точку, с которой мог бы сое- диниться клиент, хотя номер порта может создаваться для клиента динамически. Номера портов TCP и UDP имеют длину 16 битов, их можно подразделить на три категории: О Системные (известные) номера портов □ Пользовательские (зарегистрированные) номера портов □ Динамические, или частные, порты Системные номера портов находятся в диапазоне от 0 до 1023. Эти номера дол- жны использоваться только системными, привилегированными процессами. Ши- роко известные протоколы пользуются номерами портов, установленными по умолчанию из этого диапазона. Пользовательские номера портов находятся в диапазоне от 1024 до 49151. Ваше серверное приложение обычно будет занимать один из этих портов, и вы, если за- хотите сделать его известным сообществу пользователей Интернета, сможете зарегистрировать номер порта в IANA. Динамические номера портов принимают значения из диапазона от 49 152 до 65 535. Если не требуется знать номер порта до запуска приложения, подойдет порт в этом диапазоне. Клиентские приложения, которые соединяются с серверами, могут использовать такой порт. Запустив утилиту netstat с опцией -а, мы увидим перечень всех используемых в данный момент портов и указание о состоянии соединения — находится ли соеди- нение в состоянии прослушивания или соединение уже было установлено: В файле services из каталога <windir>\system32\drivers\etc перечислены многие предопределенные пользовательские и системные номера портов. Если порт содер- жится в перечне этого файла, то утилита netstat вместо номера порта отобразит имя протокола. Системные и пользовательские номера портов назначаются агентством IANA. Перечень определенных номеров портов можно найти на странице http://www.lana.org/asslgnments/port-numbers.
Сетевые понятия и протоколы 21 TCP— Transmission Control Protocol Обмен данными, ориентированный на соединения, может использовать надеж- ную связь, для обеспечения которой протокол уровня 4 посылает подтверждения о получении данных и запрашивает повторную передачу, если данные не получены или искажены. Протокол TCP использует именно такую надежную связь. TCP используется в таких прикладных протоколах, как HTTP, FTP, SMTP и Telnet. Протокол TCP требует, чтобы перед отправкой сообщения было открыто соеди- нение. Серверное приложение должно выполнить так называемое пассивное от- крытие (passive open), чтобы создать соединение с известным номером порта, и, вместо того чтобы отправлять вызов в сеть, сервер переходит в ожидание поступле- ния входящих запросов. Клиентское приложение должно выполнить активное открытие (active open), отправив серверному приложению синхронизирующий порядковый номер (SYN), идентифицирующий соединение. Клиентское приложе- ние может использовать динамический номер порта в качестве локального порта. Сервер должен отправить клиенту подтверждение (АСК) вместе с порядковым номером (SYN) сервера. В свою очередь клиент отвечает АСК, и соединение уста- навливается. После этого может начаться процесс отправки и получения сообщений. При по- лучении сообщения в ответ всегда отправляется сообщение АСК. Если до получе- ния АСК отправителем истекает тайм-аут, сообщение помещается в очередь на повторную передачу. Поля заголовка TCP перечислены в следующей таблице: Поле Длина Описание Порт источника 2 байта Номер порта источника Порт назначения 2 байта Номер порта назначения Последовательный номер 4 байта Последовательный номер генерируется источником и используется назначением, чтобы переупорядочить пакеты для создания исходного сообщения и отправить подтверждение источнику. Номер подтверждения 4 байта Если установлен бит АСК поля "Управление”, в данном поле содержится следующий ожидаемый последователь- ный номер. Смещение данных 4 бита Информация о начале пакета данных. Резерв 6 битов Резервируются для будущего использования. Управление 6 битов Биты управления содержат флаги, указывающие, верны ли поля подтверждения (АСК), указателя срочности (URG), следует ли сбрасывать соединение (RST), послан ли синхронизирующий последовательный номер (SYN) и т. д. Размер окна 2 байта В этом поле указывается размер приемного буфера. Используя подтверждающие сообщения, получатель /может информировать отправителя о максимальном размере данных, которые тот может отправить. Контрольная сумма 2 байта Контрольная сумма заголовка и данных; по ней определя- ется, был ли искажен пакет Указатель срочности 2 байта В этом поле целевое устройство получает информацию о срочности данных.
22 Глава 1 продолжение таблицы Опции переменная Необязательные значения, которые указываются при необходимости. Дополнение переменная В поле дополнения добавляется столько нулей, чтобы заголовок заканчивался на 32-битной границе. TCP — это сложный, требующий больших затрат времени протокол, что объяс- няется его механизмом установления соединения, но он берет на себя заботу о га- рантированной доставке пакетов, избавляя нас от необходимости включать эту функциональную возможность в прикладной протокол. Протокол TCP определен в RFC 793. Программирование с протоколом TCP освещается в главе 3. UDP—User Datagram Protocol В отличие от TCP UDP — очень быстрый протокол, поскольку в нем определен самый минимальный механизм, необходимый для передачи данных. Конечно, он имеет некоторые недостатки. Сообщения поступают в любом порядке, и то, кото- рое отправлено первым, может быть получено последним. Доставка сообщений UDP вовсе не гарантируется, сообщение может потеряться, и могут быть получены две копии одного и того же сообщения. Последний случай возникает, если для отправки сообщений в один адрес использовать два разных маршрута. UDP не требует открывать соединение, и данные могут быть отправлены сразу же, как только они подготовлены. UDP не отправляет подтверждающие сообщения, поэтому данные могут быть получены или потеряны. Если при использовании UDP требуется надежная передача данных, ее следует реализовать в протоколе более высокого уровня. Так в чем же преимущества UDP, зачем может понадобиться такой ненадежный протокол? Чтобы понять причину использования UDP, нужно различать однонап- равленную передачу, широковещательную передачу и групповую рассылку. Однонаправленное (unicast) сообщение отправляется из одного узла только в один другой узел. Это также называется связью “точка-точка”. Протокол TCP под- держивает лишь однонаправленную связь. Есди серверу нужно с помощью TCP взаи- модействовать с несколькими клиентами, каждый клиент должен установить соединение, поскольку сообщения могут отправляться только одиночным узлам. Широковещательная передача (broadcast) означает, что сообщение отправ- ляется всем узлам сети. Групповая рассылка (multicast) — это промежуточный механизм: сообщения отправляются выбранным группам узлов. UDP может использоваться для однонаправленной связи, если требуется быс- трая передача, например для доставки мультимедийных данных, но главные преи- мущества UDP касаются широковещательной передачи и групповой рассылки. Обычно, когда мы отправляем широковещательные или групповые сообщения, не нужно получать подтверждения из каждого узла, поскольку тогда сервер будет на- воднен подтверждениями, а загрузка сети возрастет слишком сильно. Примером широковещательной передачи является служба времени. Сервер времени отправ- ляет широковещательное сообщение, содержащее текущее время, и любой хост,
Сетевыепонятияипротоколы если пожелает, может синхронизировать свое время с временем из широковеща- тельного сообщения. Заголовок UDP гораздо короче и проще заголовка TCP: Длина Поле Описание 2 байта Порт источника Указание порта источника для UDP необязательно. Если это поле используется, получатель может отправить ответ этому порту. 2 байта Порт назначения Номер порта назначения 2 байта Длина Длина сообщения, включая заголовок и данные. 2 байта Контрольная сумма Контрольная сумма заголовка и данных для проверки UDP-это быстрый протокол, не гарантирующий доставки. J , г. Если требуется поддержание порядка сообщений и 'одежная доставка, нужно использовать TCP. UDP главным образом предназначен для М широковещательной и групповой передачи. Протокол UDP определен в RFC 786. О том, как программировать приложения с использованием UDP, рассказывается в главе 6. ICMP—Internet Control Message Protocol ICMP — это протокол управления сообщениями в Интернете, используется IP-устройствами, чтобы информировать другие IP-устройства о действиях и ошиб- ках в сети. Без TCP IP не является надежным протоколом: он не отправляет подтвер- ждения, не проверяет данные на ошибки (только контрольную сумму заголовка) и не повторяет передачу. Об ошибках можно информировать с помощью сообщений ICMP. Сообщения ICMP используются для отправки ответной реакции о состоянии сети. Например, маршрутизатор, не найдя подходящего элемента для сети в таблице маршрутиза- ции, отправляет сообщение ICMP “недостижимый пункт назначения”. Найдя луч- ший путь, маршрутизатор может послать сообщение ICMP “перенаправить”. ICMP не располагается поверх IP, как могло бы показаться, напротив, сообще- ния ICMP отправляются внутри заголовка IP. Следовательно, протокол ICMP дол- жен быть реализован модулем IP стека сети. Эти поля сообщения ICMP ставятся в начале заголовка IP: Длина Поле Описание \ 1 байт Тип В этом поле задается тип сообщения ICMP. Например, значение типа, равное В, означает, что пункт назначения - недостижим, 11 определяет, что время истекло, 12 — обнаружены некорректные параметры заголовка. 1 байт Код Код предоставляет дополнительную информацию о типе сообщения. Для типа “недостижимый пункт назначения” код указывает, что именно недостижимо: сеть (0). хост (1), протокол (2) или порт (3). 2 байта Контрольная сумма Контрольная сумма сообщения ICMP.
24 Глава 1 продолжение таблицы 4 байта Зависит от типа . последних 4 байтах заголовка ICMP может предоставляться дополнительная информация, зависящая от типа сообщения. Обычный заголовок IP С использованием ICMP можно отправить в том числе сообщения следующих типов: □ Эхо, отклик на эхо Команда ping отправляет ICMP-команду “эхо” устройству-адресату, и, если все нормально, обратно посылается “отклик на эхо”. □ Пункт назначения недостижим, перенаправить Маршрутизатор возвращает ICMP-сообщение “пункт назначения недости- жим”, если нельзя добраться до целевого устройства, и возвращает сообще- ние “перенаправить”, если обнаружен лучший путь к целевому устройству. □ Время истекло Превышено значение “время жизни” (TTL). Команда ping Утилита ping командной строки Windows отправляет ICMP-сообщение “эхо” це- левому устройству, указанному именем хоста или IP-адресом в команде ping. Если устройство достижимо, обратно отправляется сообщение “отклик на эхо”. Эта команда очень полезна, когда нужно проверить, можно ли достичь данного устройства, есть ли проблемы на пути к нему (команда PING -t продолжает посылать сообщения, пока не будет остановлена) и как быстро сообщение доходит до устройства. / Если вы не можете достичь какого-либо хоста с использованием команды ping, то это не означает, что его нельзя достичь с помощью других протоколов. ICMP-сообще- ния “эхо”могут блокироваться маршрутизаторами или брандмауэрами. На следующем снимке экрана показан вывод, создаваемый командой ping для хоста с IP-адресом 212.183.100.193. По умолчанию ping посылает узлу назначения четыре ICMP-сообщения и ждет откликов. На снимке показано, что были отправле- ны 32 байта данных и время, прошедшее до получения каждого отклика, было мень- ше 10 млс. После четырех откликов появилась сводка по результатам, из которой видно, что было потеряно 0% пакетов. Промежуточные сбои обычно приводят к потере определенной доли пакетов. Протокол ICMP определен в RFC 792.
Сетевые понятия и протоколы ___________ , ...........25 IGMP—Internet Group Management Protocol Как и ICMP, IGMP представляет расширение протокола IP и должен быть реали- зован модулем IP. IGMP используется приложениями групповой рассылки. При от- правке широковещательного сообщения в локальной сети каждый узел этой сети анализирует сообщение вплоть до транспортного уровня, чтобы проверить, хочет ли какое-либо приложение получать сообщения от порта широковещательной пе- редачи. Если ожидающие приложения отсутствуют, сообщение уничтожается и не передается выше транспортного уровня. Это означает, что каждому хосту нужно за- тратить несколько циклоь центрального процессора (ЦП) независимо ют того, интересует его широковещательное сообщение или нет. При групповой рассылке этот момент учитывается, и сообщения отправляются не всем узлам локальной сети, а только группе узлов. Сетевая интерфейсная плата может определить, интересуется ли система данным сообщением, анализируя ши- роковещательный МАС-адрес и не прибегая к помощи ЦП. t Заинтересованность в сообщениях групповой рассылки регистрируется отправ- кой запроса на членство в группе через IGMP-сообщение. Аналогично можно ис- пользовать IGMP, чтобы отказаться от членства. Об использовании IGMP можно прочитать подробнее в главе 7, где мы создадим приложения групповой рассылки для платформы .NET. Протокол IGMP определен, в RFC2236. Протоколы Интернета После обсуждения базовых протоколов мы можем подняться ria более высокий уровень. Протоколы HTTP и FTP охватывают уровни 5—7 модели OSI. FTP — File Transfer Protocol FTP используется для копирования файлов с сервера и на сервер, а также для по- лучения списка файлов и каталогов на сервере. FTP — это протокол прикладного уровня, базирующийся на TCP. Команды FTP включаются в блок данных ТСР-сооб- щения. Модель приложения с FTP-сервером и клиентом проиллюстрирована на следую- щем рисунке. Приложение-клиент представляет пользовательский интерфейс и со- здает FTP-запрос в соответствии с запросом пользователя и спецификацией FTP. FTP-команда посылается приложению-серверу через TCP/IP, и интерпретатор на сервере соответственно интерпретирует FTP-команду. В зависимости от FTP-команды в FTP-ответе клиенту возвращается с сервера список файлов или конкретный файл. Протокол FTP имеет следующие характеристики: О Надежная передача данных через TCP □ Анонимный доступ или аутентификация пользователя по имени и паролю □ Файлы отправляются в ASCII-коде в форме, поддерживаемой целевой плат- формой, или как неизмененные двоичные данные. FTP-команды можно сгруппировать в следующие категории: □ Команды контроля доступа В FTP-командах контроля доступа указывается имя пользователя (USER) и па- роль (PASS), установки могут изменяться (REIN), и соединение может быть закончено (QUIT).
26 Глава 1 □ Команды параметров передачи FTP-передачу можно конфигурировать с помощью команд параметров пере- дачи. Эти команды поддерживают изменение ASCII-кода на двоичный код, сжатие данных, изменение портов для отправки. □ Команды ITP-сервиса Копирование файлов с сервера (RETR), кдпирование файлов на сервер (STOR), удаление файлов (DELE), переименование файлов (RNTO), созда- ние каталогов (MKD) и запрос списка файлов (LIST) — вот некоторые коман- ды FTP-сервиса. Протокол FTP определен в RFC 959. FTP-клиенты Чтобы понять суть протокола FTP, лучше всего поработать из командной стро- ки с утилитой ftp, как показано на следующем рисунке. Программа ftp работает че- рез приглашение f tp>, позволяющее вводить команды. Эти команды отличаются от команд протокола FTP — вы можете увидеть их все, если введете команду ?. При вве- дении команды open ftp.fliicrosoft.com создается соединение с хостом ftp.micro- soft.com. Установка имени пользователя anonymous означает пользователя-гостя. Ответ 230 от сервера указывает, что соединение установлено и можно командой dir получить список файлов на сервере. Получив команду di г, программа ftp отправля- ет на сервер команду FTP LIST. Команда cd может применяться для смены директо- рии на сервере, а команда get копирует файл клиенту, отправляя команду FTP RETR. Для закрытия соединения утилита ftp использует команду bye:
Сетевые понятия и протоколы 27 Cl'.WlNMF»-. tem32\cn»dexe ftp) open ftp.nicrosofe.con Connected to ftp.nicrosoft.cor. 220 cpnsftftpa06 Microsoft FTP Service <Uersion 5.0). d ci <Ftp.nLcrosoft.con* (none)): anony.touu 331 Anonymous access allow, d, send identity (e-mail namu) as password. Pass* ord' |2 10 fliis is FTP Microsoft .Con. [230 Anonymous user logged in. |ftp) dir 200 PORT i.onnand successful. 150 Opening dr-xr-xr-x ASCII node 1 owner 1 ouner 1 owner 1 owner 1 ouner 1 owner 1 ouner 1 owner 1 owner 1 ouner 1 ouner conplete. data connection group group group group group group group group group group group for zbin/ls 0 ' ‘ 3 0 0 0 0 0 0 0 d 0 1226 Transfer _________ jfty 745 bytes received .ftp) cd developr 1250 CUD command successful. Up) get readne.txt 200 PORT connand successful. 150 Opening ASCII node data connection 226 Transfer conplete. ftp: 1427 bytes received in 0.13Scconds 10.98Kbytes/oec it pi bye Thank-you for using Microsoft products! in 0,03Secondi *221 Jul 2 12:23 bussys Мау 21 2001 deskapps Apr 2Г 2001 developr Feb 25 2000 KBHelp Jul 2 13:13 MISC Jul 3 14:02 MISCI Feb 25 2000 peropsyc Jan 2 2001 Products Sep Feb 21 2000 ResKit 25 2000 Services Feb 25 2000 Softlib 24,83Kbytes/sec for readme.txt<1427 bytes) Еще один FTP-клиент— Microsoft Internet Explorer. Вместо использования URL в виде http://hostname, мы запускаем FTP-клиент с идентификатором схемы ftp://. Этот инструмент позволяет копировать файлы буксировкой: \
28 Глава 1 HTTP—Hypertext Transfer Protocol HTTP — основной протокол, используемый Web-приложениями. Как и FTP, HTTP является надежным протоколом, и надежность его достигается благодаря ис- пользованию TCP. Как и FTP, H'ITP также используется для передачи файлов через сеть. Но он в отличие от FTP обладает такими средствами, как кэширование, иден- тификация приложения-клиента, поддержка разных дополнений в формате MIME и т. д. Эти средства устанавливаются в заголовке HTTP. Для демонстрации работы, выполняемой браузером Интернета, когда он запра- шивает файлы из Web-сервера, мы можем имитировать браузер приложением telnet. Для запуска этого приложения введите telnet в диалоговом окне Run меню Start, и вы увидите приглашение Microsoft Telnet>. Введите set local_echo (set localecho для Windows XP), чтобы введенные программы отображались локально. Если не устанавливать эту опцию, команды, которые мы отправляем на сервер, не будут отображаться приложением telnet. Теперь можно соединиться с Web-серве- ром командой open. Команда open msdn.microsoft.com 80 создает ТСР-соединение с портом 80 сервера на msdn.microsoft.com. Приложение telnet использует по умол- чанию порт 23, следовательно, надо указать пЬрт для HTTP-запроса. По умолчанию HTTP-сервисы предоставляются портом 80 Web-сервера. I ; WIN4T System32' emdexe temet Microsoft <R> Windows 2000 <TM> Uersion S.00 <Build 2195> Welcome to Microsoft Telnet Client Telnet Client Build S.00.99203.1 Escape Character is 'CTRL**' Microsoft TelneO set local-echo Microsoft Telnet> open msdn.MicrosoFt.coM 8b— Как только соединение инициировано, можно послать на Web-сервер НТТР-за- прос. Простой запрос состоит из строки запроса, которую нужно завершить двумя нажатиями на клавишу Enter (две последовательности CR-LF). Такая строка запроса • может выглядеть, как на следующем снимке экрана: GET /default. asp НТТР/1.0. Сер- вер возвращает HTTP-ответ, содержащий информацию о статусе (в данном случае 200 ОК) и за ней HTML-содержание: Sdec* C\wiwr Syrtcm32 стЛеке 1 GET /default.asp HTTFZ1.0 IHTIP/1 t 200 OK Connection: close Dote. Il hi. 25 Jul 2002 11:56:40 GMT ' truer: Microsoft-1IS/6.0 !P3P: oolicyref“"http.Z/wwu.Microsoft.сом/мЗс/рЗр.хнГ* CP“"ALL IHD DSP COR ADM C Ho CUR CUSo lUAo IJDo PSA PSD TA) TELo OUR SAM'' CUT COM 1NT HAU OHL PHV PRE PUR UHI" Concent-Length: 49279 ^ontent-Type textzhtnl Cache -control: private <HTNL> <HEAD> <META HTTP-EOUIU-"Content-Type" COHIEHT-"text/htnl; cl>arset-UTF-8"/ <TITLE>MSDN Hone</TITLE> СМЕТА HAHE-'Description" С0Н1ЕНГ-"ttSDH Номе’» <ИЫА ГАН "Robots"’ СОНТЕНТ-''all"» <META HA ME-''Keywords” CONTENT-"MSDH. Microsoft. devolo er. network, developer resources. Microsoft developer resources'» <МЕТя HAME-"B LOCALE' CONTENT-"en-us"» ,?Г?>4 Как видно, элементарный HTTP-запрос состоит лишь из одной строки. Однако полный HTTP-запрос будет состоять из строки запроса с дополнительными заголов- ками и данными.
Сетевые понятия и протоколы , 29 V В строке запроса можно указывать такие НТГР-команды, как GET, HEAD и POST. И GET, и POST запрашивают данные от сервера. Команда GET включает параметры за- проса в URL, а в команде POST параметры находятся в блоке данных. Команда HEAD означает, что мы просто хотим узнать, когда был изменен запрашиваемый файл, чтобы проверить, находится ли в кэше новейшая его версия. За строкой запроса могут следовать общие заголовки, заголовки запроса и заго- ловок сущности. Информация, помещаемая в заголовки, позволяет клиенту сооб- щать серверу об используемом браузере и предпочтительных языках, отправлять файл cookie или запрашивать только изменившиеся файлы. В примере с telnet мы уже виДели некоторую информацию из заголовков, возвращенную сервером: дату, версию сервера, длину содержания, тип содержания и признаки управления кэшем. Более подробную информацию о протоколе HTTP и программировании с использованием протокола HTTP можно найти в главе 8. HTTP определен в RFC 1945. HTTPS—HTTP поверх SSL (Secure Socket Layer) Когда требуется обменяться с Web-сервером конфиденциальной информацией, можно воспользоваться протоколом HTTPS. HTTPS — это расширение протокола HTTP, и поэтому к нему применимы все принципы, которые обсуждались в преды- дущем разделе. ОдНако в его основе лежит другой механизм, поскольку HTTPS использует SSL, первоначально разработанный компанией Netscape. SSL располага- ется поверх TCP и защищает сетевой обмен, используя принцип открытого/секрет- ного ключа для обмена секретными симметричными ключами и симметричный ключ для шифрования сообщений. Для поддержки HTTPS Web-сервер должен установить сертификат, чтобы его можно было идентифицировать. По умолчанию для HTTPS-запросов используется порт 443. За более подробной информацией можно обратиться на Web-сайт Netscape к следующей странице: http://wp.netscape.com/eng/ssl3/ssl-toc.html. / Протоколы электронной почты Для электронной почты разработано довольно много протоколов. В этом разде- ле представлен обзор наиболее важных протоколов, имеющих отношение к почте. В главе 8 рассмотрим их подробнее и увидим, как создать использующее их прило- жение. SMTP—Simple Mail Transfer Protocol SMTP — это протокол, предназначенный для отправки и получения сообщений электронной почты. Он может использоваться для пересылки электронной почты между клиентом и сервером, использующими один и тот же транспортный про- токол, или для пересылки сообщений между серверами, использующими разные транспортные протоколы. SMTP имеет возможность пересылать сообщения через среду транспортной службы. Однако SMTP не позволяет читать сообщения с почто- вого сервера, для этого используются протоколы POP3 и IMAP. Служба SMTP составляет часть установки Internet Information Server в Windows 2000 и Windows-XP. Стандарт протокола SMTP определен в RFC 821; формат сообщений SMTP определен в RFC 822.
30 Глава 1 POP3 — Post Office Protocol v Протокол POPS предназначался для отсоединенной среды. В небольших конфи- гурациях непрактично поддерживать постоянное соединение с почтовым сер- вером, например в такой среде, где время соединения нужно оплачивать. При использовании POP3 клиент может обращаться к серверу и извлекать сообщения, которые хранит для него сервер. Когда сообщения считываются клиентом, они обычно (но необязательно) удаляются с сервера. Windows .NET Server включает сервер POP3. Протокол POP3 определен в RFC 1081. IMAP—Internet Message Access Protocol Как и POP3, протокол IMAP предназначен для доступа к почте на почтовом сер- вере. Аналогично клиентам POP3 клиент IMAP может работать в автономном ре- жиме, в котором почта обрабатывается на локальной машине. По сравнению с клиентами POP3 клиенты IMAP обладают более широкими возможностями в опе- ративном режиме, например, они могут извлекать только заголовки или только основные части указанных почтовых сообщений, искать конкретные сообщения на сервере и устанавливать флаги, например флаг “ответ отправлен". По существу, IMAP позволяет клиенту обрабатывать удаленный почтовый ящик, как если бы он был локальным. Протокол IMAP определен в RFC 1730. NNTP—Network News Transfer Protocol NNTP — это протокол прикладного уровня для передачи, ретрансляции и извле- чения сообщений, являющихся частью обсуждений в группах новостей. Этот про- токол обеспечивает приложения-клиенты доступом к серверу новостей для извлечения выбранных сообщений и поддерживает передачу сообщений между серверами. Протокол NNTP определен в RFC 850, 977 и 1036. Другие прикладные протоколу Существуют еще два интересных прикладных протокола: SNMP и Telnet. Simple Network Management Protocol (простой протокол сетевого управле- ния) нацелен на управление устройствами в сети. В разнообразной информации, например в счетчиках производительности от устройств, недостатка нет. Наобо- рот, информации слишком много, но нужно ею управлять эффективно. SNMP пред- назначен для эффективного управления устройствами с помощью аварийных сигналов, которые срабатывают из-за возникающих проблем и нарушений, связанных с производительностью, и позволяет конфигурировать устройства. Агент SNMP, связанный с конкретным сетевым устройством, будет обладать ба- зой управляющей информации (Management Information Base, или MIB) — базой данных, содержащей всю допускающую управление информацию для этого устро- йства в объектно-ориентированном стиле (т. е. Состоящей из объектов, атрибутов и экземпляров). Клиент SNMP обращается к информации в этой базе данных, от- правляя SNMP-запросы GET. Напротив, SNMP-запросы SET используются для кон- фигурирования базы данных MIB.
Сетевые понятия м протоколы 31 При ошибках или проблемах с производительностью агент SNMP отправляет клиенту SNMP trap-сообщения. Протокол SNMP определен в RFC 1157. База данных MIB определена в RFC 1155 и RFC 1156. Ранее использовалось приложение telnet, чтобы имитировать браузер и его вы- полнение HTTP-запроса. Однако приложение telnet в первую очередь предназ- начено для соединения с сервером telnet через протокол Telnet. Этот протокол позволяет соединяться с удаленной системой, используя аутентификацию пользо- вателя, и затем вызывать удаленно команды из среды консоли. Сокеты Термин сокет (socket) не определяет протокол. У него есть два значения, но ни одно из них не имеет отношения к протоколу. Одно значение — это API программи- рования сокетов, первоначально созданный в Университете Беркли для BSD UNIX. Программный интерфейс BSD-сокетов был адаптирован для среды Windows и полу- чил название WinSock. API WinSock заключен в классы .NET пространства имен System. Net. Sockets. Windows Sockets — это независимый от протокола программный интерфейс для написания сетевых приложений. Подробнее о программировании сокетов можно узнать из глав 4-6. В последних главах, где демонстрируются классы высокого уровня для интернет-про-' граммирования, сокеты используются неявно. Второе использование термина “сокет” относится к обозначению конечной точ- ки для связи между процессами. В TCP/IP конечная точка связана с IP-адресом и номером порта. Различаются потоковый и дейтаграммный типы сокетов. Пото- ковый сокет использует ориентированную на соединение связь и протокол TCP/IP. С другой стороны, дейтаграммный сокет использует связь без соединения и UDP/IP. Подробнее сокеты рассматриваются в главе 4. Имена доменов Совсем непросто запомнить IP-адреса в десятичной нотации, и поэтому хостам в сети даны более удобные для пользования имена. Поскольку эти имена должны быть уникальными, используемая система доменных имен поддерживает иерархию имен. Вот несколько примеров таких имен: www.wrox.com,msdn.microsoft.com, ker- beros.vienna.globalknowledge.com. Эти имена необязательно состоят из трех частей, но при чтении справа налево имя начинается с домена верхнего уровня. Эти домены верхнего уровня зависят от страны (например, .com.tw) или являются общими (как
32 , - Глдва1 .org) и определяются агентством IANA. Имя, появляющееся непосредственно слева от домена верхнего уровня, представляет имя домена. За поддержание уникальности левее этого имени отвечает лицо или организация, владеющие доменом. Общие домены верхнего уровня перечислены в следующей таблице. За послед- ние годы появилось несколько новых имен доменов верхнего уровня: Имя домена Описание .аего Авиационная промышленность .biz Бизнес .com Коммерческие организации .coop Кооперативные ассоциации .info Нет ограничения в использовании .museum Музеи .name Отдельные лица Имя домена Описание .net Сети •org Некоммерческие организации .pro Профессионалы .gov Правительство США .edu Учебные заведения .mil Вооруженные силы США .int Организации, учрежденные международными договорами между правительствами Детальное описание этих общих доменов верхнего уровня, спонсоров и регистраторов доменов можно найти по следующему URL: http://www.lana.org/gtld/gtld.htm. Дополнительно к общим доменам верхнего уровня существует доменное имя для каждой страны: Имя домена Описание .at Австрия ,сс Кокосовые острова (Килинг) .de Германия .fr Франция .tv Тувалу .uk Соединенное Королевство Полный перечень доменов стран находится на странице http://www.iana.org/cctld/cctld-whois.htm.
Сетевые понятия и протоколы 33 Служба whois Служба whois (или “кто есть кто”) обеспечивает средства для поиска в службе ре- гистрации лиц и организаций, зарегистрировавших конкретный домен, их контак- тной информации, адресов регистрации и т. д. Такая служба whois имеется на Web-сайте http://www.internic.net. Серверы доменных имен Имена хостов разрешаются при помощи серверов DNS (Domain Name Service). Эти серверы содержат базу данных имен хостов и алиасных имен (имен-псевдони- мов), ставящих в соответствие IP-адресам имена. Серверы DNS также регистрируют информацию о почтовых серверах, номерах ISDN, именах почтовых ящиков и сервисах. В Windows именно параметрами настройки TCP/IP задается, какой сервер DNS должен использоваться для запросов, и команда ipconfig /all показывает установ- ленные серверы DNS и другие параметры конфигурации. Когда для установления соединения с удаленной системой используется некоторое имя хоста, с сервера DNS получается соответствующий IP-адрес. Сервер DNS сначала проверяет соб- ственную базу данных и кэш, и, если ему не удается разрешить имя, он запрашивает корневой сервер DNS. По всему миру существует несколько корневых серверов (имеющих имена от а.root-servers.net до rn.root-servers.net), которые могут обра- щаться к серверам DNS доменов верхнего уровня. Сервер DNS домена верхнего уровня знает сервер DNS для конкретного поддомена и поэтому возвращает IP-ад- рес, соответствующий конкретному имени хоста. Для ускорения обработки следующих запросов серверы DNS сохраняют в кэше информацию, не найденную в их базе данных. Nslookup Утилита nslookup командной строки предоставляет IP-адреса для имен хостов, направляя запросы к принятому по умолчанию серверу DNS. В случае, показа] 'ном на следующем снимке экрана, сервер DNS имеет имя WS01IS02.highway.telekom.at. Когда запрашивается у этого сервера IP-адрес для www.microsoft.com, он возвраща- ет реальное ймя сервера www,microsoft.akadns.net, шесть IP-адресов, сконфигури- рованных для этого сервера и его алиас www.microsoft.com: В данном примере получен неофициальный ответ, это значит, что сервер не от- вечает за домен, запрос о котором был сделан, и читает информацию о нем из свое- го кэша. Кстати, кэширование служит причиной многих проблем, возникающих при поиске имени хоста. Сервер доменных имен редко чистит свой кэш DNS, и в ре-
34 Глава 1 зультате разные серверы содержат противоречивую информацию. Несколько дней может уйти на то, чтобы изменение адреса стало известно во всем Интернете. Ути- лита nslookup позволяет установить для запроса другой сервер DNS, и поэтому можно сравнить информацию, полученную от разных серверов. Интернет В этой главе освещены многочисленные базовые технологии, поддерживающие Интернет: оборудование, протоколы и систему доменных имен. Осталось обсудить еще несколько интересных тем: □ Интрасети и экстрасети □ Брандмауэры и Web-прокси □ XML Web-сервисы Интрасети и экстрасети В интрасети (intranet) технологии TCP/IP могут использоваться примерно так же, как в Интернете. Ее отличие, конечно, состоит в том, что это частная сеть, все пользователи которой известны. Интрасеть не предназначена для общего открыто- го доступа, и некоторые данные, если не все, должны защищаться от обращений из- вне. Полностью защитить интрасеть от доступа из Интернета—это задача, которую выполняют брандмауэры: Экстрасеть (extranet), как и интрасеть, тоже является частной сетью, но экс- трасети соединяют несколько участков интрасетей, принадлежащих одной ком- пании или компаниям-партнерам, через Интернет с использованием туннеля (tunnel). Создание таким способом виртуальной частной сети (Virtual Private Net- work, или VPN) через Интернет дает значительную экономию затрат по сравнению с арендой частных линий. Брандмауэры Итак, брандмауэры (межсетевые экраны) защищают интрасеть от того беспо- рядка и той опасности, которые представляет Интернет. Часто брандмауэры иполь- зуются для защиты подсети внутри компании от других подсетей и для ограничения доступа к первой подсети только для указанных пользователей или задач.
Сетевые понятия и протоколы 35 Чтобы защитить сеть брандмауэром, ее нужно сконфигурировать так, чтобы весь входящий и исходящий трафик проходил через брандмауэр. Естественно, не должно иметься альтернативных маршрутов, по которым можно было бы обойти брандмауэр. Брандмауэры могут работать на разных уровнях модели OSI. Фильтры пакетов проверяют пакеты ифильтруют их в соответствии с IP-адресами и номерами портов сети и транспортных уровней. Пакетам, направленным от определенных IP-адре- сов и к определенным IP-адресам, может быть разрешено пройти через брандмауэр или в Сеть, или из нее во внешний мир. Для фильтрации пакетов могут использо- ваться номера портов, чтобы указать, какие службы можно использовать с обеих сторон брандмауэра. Например, брандмауэр можно установить так, чтобы сторона Интернета (называемая “красной стороной” в терминологии брандмауэра) могла обращаться через HTTP только к Web-серверам, по определенным IP-адресам за брандмауэром. Такая конфигурация представлена на следующем рисунке, где вто- рой брандмауэр защищает внутреннюю сеть компании, содержащую почтовый сер- вер, файловые службы и рабочие станции пользователей. Если приложениям, выполняемым на Web-сервере, нужен доступ к данным из интрасети, для второго брандмауэра можно сконфигурировать специальные протоколы и порты. Если определены защитные фильтры с номером порта, то каждый пакет, от- правленный через брандмауэр, должен передаваться транспортному уровню для проверки. Более высокий уровень проверки возможен при использовании фильтров приложений. Фильтр приложения должен знать о командах протокола прикладного уровня, таких как FTP, Н АТР иди SMTP, и может, например, позво- лить копирование определенные файлов из Интернета в интрасеть, но не наобо- рот, может разрешить команды FTP GET, но не FTP PUT. Фильтры приложений для SMTP часто отвергают почту, использующую команду DEBUG, поскольку такая почта может использоваться для проникновения в локальные сети. Web-прокси Web-прокси (web proxy) кэширует Web-запросы клиентов. Интернет-браузеры в интрасети можно сконфигурировать так, чтобы они использовали Web-nрокси, направляющий НТТР-запрос Web-серверу в Интернет. Web-прокси может кэширо- вать Web-запросы, чтобы последующие запросы клиентов на те же страницы полу- чали ответ не от Web-сервера, а из кэша, в котором Web-прокси хранит полученные страницы. Еще одна функция Web-прокси состоит в ограничении доступа к определенным Web-сайтам и в регистрации Web-запросов, сделанных пользователями. Microsoft Internet Security and Acceleration Server (ISA) действует и как брандмауэр, и как Web-прокси, защищая сеть и повышая ее производительность.
36 Глава 1 XML Web-сервисы XML Web-сервис — это приложение, которое можно идентифицировать по URI и вызвать дистанционно с помощью таких удобных протоколов Интернета, как HTTP. В основе XML Web-сервисов лежит протокол SOAP (Simple Object Access Pro- tocol). SOAP определяет формат XML для вызова удаленных методов способом, не зависящим от технологий, использованных для реализации этих мегодов. Типичный процесс вызова Web-сервисов показан на следующем рисунке: Среда проектирования Приложение- клиент Сначала мы должны найти Web-сервис, удовлетворяющий нашим требованиям, для чего можно опросить UDDI-сервер, используя протокол UDDI (Universal Desc- ription, Discovery and Integration). Открыто доступные Web-сервисы могут быть зарегистрированы на таком UDDI-сервере, как http://www.uddi.org или http://www.salcentral.com. обеспечивающем возможности поиска. UDDI-сервер возвращает определенную информацию о Web-сервисах, соответствующих указан- ным требованиям, например в виде ссылки на документ WSDL (Web Service Description Language), детализирующий методы, представленные Web-сервисом в читаемом компьютером XML-формате. Если Web-сервис должен быть недоступен, его WSDL-документ может предоставляться другими способами или с закрытого UDDI-сервера. Компания Microsoft Предоставляет UDDI-сервер, который может быть установлен с Windows .NET Server. Методы и параметры, которые описываются WSDL-документом, используются для построения SOAP-запросов для вызова Web-сервиса. ASP.NET предоставляет простой способ создания Web-cepeuca с использованием классов из пространства имен System. Web. Services. Спецификацию SOAP можно найти по URL http://www. w3.org/20QQ/xp/Group/. .NET Remoting Протокол удаленного вызова процедуры (Remote Procedure Call, или RPC) был первым широко признанным способом вызова функций через сеть. RPC — это про- токол высокого уровня, который не требует, чтобы клиент создавал сообщение и отправлял его получающей стороне, после чего забирал сообщение с сервера, ана- лизировал его, а затем уже вызывал нужную функцию. Вместо этого прикладной программист может вызвать функцию напрямую на сервере. RPC-прокси, выполня- ющийся на клиенте, производит маршаллинг удаленного вызова метода (т. е. преоб-
Сетевые понятия и протоколы 37 разует его в сетевое сообщение), отправляет его на сервер, где RPC-заглушка (stub) разбирает сообщение и вызывает метод. Поскольку RPC был ориентирован на функции, модель распределенных объек- тных компонентов DCOM (Microsoft Distributed Component Object Model) расши- рила его, чтобы добавить объектную ориентацию. Вместе с .NET появляется новая модель распределенных приложений, наслед- ница DCOM—.NET Remoting. По сравнению с DCOM .NET Remoting предлагает го- раздо большую гибкость и расширяемость. Следующий рисунок иллюстрирует общую архитектуру .NET Remoting. Удален- ный объект располагает несколькими методами для удаленного вызова. На стороне клиента создается прокси-объект, который зеркально отображает удаленный объ- ект, предлагая те же самые открытые методы. Клиент вызывает эти методы из прокси-класса, а прокси использует форматтер (formatter), который формирует сообщения так, чтобы их можно было послать через сеть. Сетевой транспорт определяется каналом. На сервере другой форматтер де- форматирует сообщения и передает их диспетчеру, вызывающему методы удален- ного объекта: В определенных точках потока на клиентской или серверной стороне .NET Remoting разрешает поместить перехватчики, или приемники (sinks), чтобы доба- вить такие функциональные возможности, как регистрация в журнале, дублирова- ние вызовов для повышения надежности или динамический поиск серверов. .NET Remoting поддерживает разнообразные каналы и форматтеры. Среда .NET Framework 1.0 предлагает каналы TCP и HTTP, форматтеры SOAP и двоично- го кода. Если выбрать канал НТ ГР и, форматтер SOAP, .NET Remoting превратится в XML Web-сервисы. Канал TCP с двоичными форматтерами — это быстрый меха- низм связи. Если клиент и сервер снабжены технологиями .NET, то Remoting явля- ется быстрым и несложным способом использования механизмов связи. Подробнее о XML Web-сервисах можно прочитать в книгах издательства Wrox. Professional XML Web Services (ISBN 1-86100-509-1), ProfessionalASP.NET Web Services (ISBN 1-86100-775-2) и C# Web Services (ISBN 1-86100-439-7). В книге Professional XML Web Services основное внимание уделяется независимости от поддерживающей технологии, в то время как в книге Professional ASP.NET Web Services упор делается на Web-сервисы в ASP.NET, а книга C# Web Services посвя- щена более техническим вопросам, в ней для реализации Web-сервисов используются как ASP.NET, так и .NET Remoting.
38 Глава 1 Передача сообщений Передача сообщений (messaging) — это процесс отправки сообщений от клиента серверу. Все сетевые протоколы, о которых уже говорилось, требуют соединенной среды. Не имеет значения, используются ли сокеты с TCP или UDP, HTTP или .NET Remoting, клиент и сервер должны функционировать взаимосвязанно, т. е. одно- временно. При организации очередей сообщений клиент и сервер могут действо- вать асинхронно, и клиент может посылать сообщения, даже если соединение с сервером недоступно. Сообщение будет поставлено в очередь и попадет на сервер через некоторое время. Организация очередей сообщений также дает простой спо- соб установить приоритеты для сообщений, что может быть полезно и в соединен- ной среде, если потребуется читать сообщения с более высоким приоритетом в первую очередь. Организация очередей сообщений может быть особенно полезна, когда прило- жение выполняется на переносном компьютере, который не соединен с сетью ком- пании и принадлежит, возможно, продавцу, находящемуся у покупателя. В этом сценарии организация очередей сообщений позволяет приложению отправить со- общение и хранить его в очереди сообщений клиента до следующего подсоедине- ния к сети. Что касается приложения, то оно отправляет сообщение немедленно. Пространство имен System. Messaging включает классы .NET, обеспечивающие очереди сообщений. Подробнее о программировании для платформы .NET с использованием очередей сообщений можно прочитать в TcwwzeData-Centric .NET Programming with C# (Wrox Press, ISBN 1-86100-592-X). Другие способы доступа к сетевым объектам Облегченный протокол службы каталогов (Lightweight Directory Access Protocol, или LDAP) предназначен для организаций иерархических хранилищ долговечных объектов. LDAP, или Active Directory, создает хранилища объектов, представляю- щих интерес для компании. К таким объектам могут относиться пользователи, группы, компьютеры, принтеры, сетевые совместно используемые ресурсы и спе- циализированные типы объектов. LDAP позволяет читать, записывать и искать объекты в хранилищах. Классы из пространства имен System. Directoryservices по- зволяют из среды .NET Framework обращаться к объектам в Active Directory или в других LDAP-хранилищах данных. Пространство имен System Management предоставляет классы для доступа к клас- сам инструментальных средств управления средой Windows (Windows Management Instrumentation, или WMI). WMI позволяет выполнять через сеть функции управле- ния: обращаться к информации оборудования, конфигурировать и администри- ровать службы, предлагаемые провайдерами WML WMI можно использовать для
Сетевые понятия и протоколы 39 получения информации об оборудовании, например, о свободном месте на диске, использовании центрального процессора, данных о производительности, серверах DNS и конфигурации служб терминалов. За более подробной информацией о доступе к объектам Active Directory обратитесь к книге издательства Wrox Data-Centric .NET Programming withC# (ISBN 1-86100-592-X). Организации и стандарты Интернета Существует огромное количество комитетов по стандартам, разрабатывающих сетевые спецификации и стандарты. В следующей таблице перечислены наиболее важные группы из этой области: Организация по стандарти- зации Определение Web-сайт Технологии ISO International Organization for Standardization http://www.iso.org l Международная организация по стандартизации определила сеть OSI. В настоящее время модель OSI получила широкое распространение. IEEE . Institute of Electrical and Electronic Engineers http://www.ieee.org IEEE отвечает за стандарты по локальным сетям и спецификации оборудования: Ethernet, Token Ring, MAN, Wireless LAN, Broadband. IAB Internet Architecture Board http://www.iab.org IAB отвечает за редакции RFC и назначает председателя IETF. IETF Internet Engineering Task Force http://www.ietf.org В IETF можно найти стандарты Интернета—RFC. IANA Internet Assigned Numbers Authority http://www.iana.org Как следует из названия этой организации, IANA назначает номера для Интернета: диапазоны зарезервированных IP-адресов, номера портов, номера протоколов и т. д. W3C World Wide Web Consortium http://www.w3.org W3C активно разрабатывает интернет-технологии: HTTP, HTML, XML, SOAP ит. д.
40 Главой Итоги В этой главе мы проработали основы использования сетей, предоставив оозоп наиболее важных понятий организации сетей и сетевых протоколов. Мы начали с обсуждения физической сети, рассмотрели назначение и функционирование важ- нейших компонентов сети: сетевых интерфейсных плат, концентраторов, маршру- тизаторов и коммутаторов. Еще одна фундаментальная тема, на которой основаны многочисленные работы в области сетей, — это магическая семиуровневая модель OSI. В эту модель входят сверху вниз семь уровней: прикладной, представительский, сеансовый, транспор- тный, сетевой, канальный и физический. Мы внимательно рассмотрели заголовки ключевых протоколов — IP, TCP и UDP— чтобы понять связь, ориентированную на соединения, и связь без установ- ления соединений. Мы обсудили назначение некоторых важных прикладных протоколов, а именно HTTP, FTP, SMTP и IMAP. Это лишь немногие технологии, применяемые в Интер- нете и других сетях. С такой подготовкой мы сможем в следующей главе перейти к написанию про- грамм, обрабатывающих потоки, которые очень полезны в сетевом программиро- вании. Собственно сетевое программирование начнем в главе 3.
ГЛАВА 2 Потоки в .NET оток (stream) — это абстрактное представление последовательного устрой- ства, для которого сохранение и считывание данных выполняется побайтно. Базо- вым устройством для потока может быть, например файл, принтер или сетевой сокет. Через эту абстракцию можно из одного и того же процесса обращаться к раз- ным устройствам, и аналогичный программный код может использоваться для чте- ния данных из файлового и сетевого входных потоков. Тем самым программисту не требуется беспокоиться о реальном физическом Механизме такого устройства. В этой главе мы рассмотрим следующие темы: О IIotokhb.NET □ Класс St ream и его члены □ Класс FileStream и другие классы, производные от класса Stream □ Чтение и запись двоичных и текстовых файлов □ Сериализация Потоки в .NET • Для выполнения операций над разнообразными типами потоков среда .NET Framework обеспечивает обширный набор классов. Основным является класс Stream, абстрактный класс, от которого порождены все другие связанные с потока- ми классы. Поскольку поток является абстрактным представлением данных в виде последовательности байтов, чтобы обрабатывать эти последовательности, нужно выполнять такие основные операции, как чтение, запись и поиск. В классе Stream можно выполнять над потоками операции ввода/вывода двоичных данных. В клас- сах TextReader и Textwriter выполняются операции ввода/вывода символьных дан- ных, а в классах BinaryReader и BinaryWriter —/ операции ввода/вывода простых типов.
42 Глава 2 Синхронный и асинхронный ввод/вывод Существуют два способа выполнения операций с потоком, синхронный или асинхронный — выбор зависит от требований вашего приложения. Далее узнаем, что класс Stream обеспечивает методы как для синхронных, так и для асинхронных операций, но сначала обсудим некоторые преимущества и недостатки каждого типа операций. Синхронный ВВОД/ВЫВОД По умолчанию все операции с потоками выполняются синхронно, и это про- стейший способ для операций ввода/вывода. Недостаток синхронного вво- да/вывода состоит в том, что обработка блокируется до завершения операции ввода/вывода и лишь затем приложению разрешается продолжить обработку. Синхронный ввод/вывод бывает полезен при небольших размерах файлов, но для больших файлов из-за блокирования выполнения текущего потока производи- тельность приложения может оказаться слишком низкой. Синхронный ввод/вы- вод не подходит для выполнения операций в сети, где слабо влияние на время, необходимое для завершения операции. Следовательно, синхронный ввод/вывод был бы неудачным выбором для передачи больших потоков через сеть с низкой про- пускной способностью или скоростью. Вводя многопоточную обработку (threading) в синхронные методы, можно имитировать асинхронный ввод/вывод. Асинхронный ввод/вывод При асинхронном вводе/выводе до завершения операции ввода/вывода могут выполняться другие задачи. Когда операция ввода/вывода завершается, операци- онная система уведомляет об этом вызывающую программу. Следовательно, для асинхронного ввода/вывода требуется отдельный механизм уведомления. Этот метод полезен, когда одновременно с передачей больших объемов данных из потока приложению требуется продолжать выполнение других задач или рабо- тать с медленными устройствами, чья скорость могла бы в противном случае замед- лить работу приложенйя. При асинхронном вводе/выводе для каждого запроса ввода/вывода создается отдельный поток выполнения, и это может привести к повышению накладных рас- ходов для операционной системы. Класс Stream Класс Stream из пространства имен System. 10 является базовым классом для всех классов потоков и обеспечивает функциональные возможности для выполнения основных операций с потоками. Постигнув функциональность класса St ream, вы без труда поймете его производные классы и, самое главное, научитесь создавать раз- личные типы потока с помощью каждого класса. Если говорить коротко, все классы, производные от класса Stream, представляют разные типы потоков с общими, или производными, методами класса Stream и некоторыми дополнительными методами для выполнения операций на этом конкретном типе потока. На следующей схеме иллюстрируется класс St ream, производные классы и раз- личные другие классы, выполняющие операции с потоками. Каждый производный класс характеризуется в соответствии со своим базовым устройством или поддерживающим запоминающим устройством. Например, класс
Потоки в .NET 43 FileStream для хранения потоков использует файлы, а класс Networkstream не имеет никакого поддерживающего запоминающего устройства — он создан специально для передачи потоков через сеть. В следующей таблице показаны некоторые основные классы, производные от класса Stream, и описывается их назначение: Класс Назначение FileStream Использует файлы как внешнюю память. Наиболее широко используемый St ream-класс. BufferedStream В качестве поддерживающего запоминающего устройства использует буфер. Применяется для промежуточного хранения с целью повышения производительности. Memorystream Использует основную память как поддерживающее запоминающее устройство. По сравнению с другими потоками выполняет самые быстрые операции ввода/вывода. Networkstream CryptoStream Не имеет поддерживающего запоминающего устройства. Используется с другими потоками для передачи данных через сеть. Используется с другими потоками для выполнения на потоках операций шифрования-дешифрования. Теперь рассмотрим члены класса St ream.
Глава 2 Члены класса Stream Начнем с открытых (public) свойств. Прежде всего имеются три открытых сво- йства, которые сообщают, поддерживает ли поток конкретное средство; это можно определить, когда поток создан соответствующим конструктором: Свойство Описание CanRead Через это свойство можно проверить, поддерживает ли текущий поток чтение. Вообще, оно используется перед выполнением любой операции чтения потока; возвращает значение true, если поток поддерживает чтение, иначе возвращает false. Если поток не поддерживает чтение и делается попытка его прочитать, то порождается исключение NotSupportedException. CanSeek Поиск используется для установки позиции в текущем потоке. Свойство CanSeek проверяет, поддерживает ли текущий поток операцию поиска. Оно возвращает true, если поток поддерживает поиск, иначе возвращает false. Способность потока выполнять операцию поиска зависит от его поддерживающего запоминающего устройства. Такие носители, как диск или память, как правило, будут поддерживать поиск, но потоки, не имеющие поддерживающих запоминающих устройств, всегда будут возвращать значение false. При попытке поиска для потока, который его не поддерживает, порождается исключение NotSupportedException. CanWrite Перед выполнением любой операции записи на текущем потоке используется свойство CanWrite, чтобы проверить, поддерживает ли поток запись. Это свойство возвращает true, если поток поддерживает запись, иначе возвращает false. Попытка записи в поток, который не поддерживает запись, порождается исключение NotSupportedException. Еще два свойства позволяют определить размер потока и текущую позицию в по- токе: Свойство Описание Length Это свойство, доступное только на чтение, возвращает значение long, представляющее длину потока в байтах. Оно может использоваться для проверки конца потока или для определения размера массива байтов — буфера для сохранения потока. Position Свойство Position можно использовать, чтобы получать или устанавливать текущую позицию в потоке и, таким образом, перемещаться внутри потока. Чтобы использовать это свойство, поток должен поддерживать поиск, что можно проверить с помощью свойства CanSeek, которое мы обсудили ранее У класса St ream есть несколько методов для передвижения по потоку, чтения и записи потока и управления потоком. Рассмотрим сначала метод Seek(), позволяющий передвигаться по потоку. Ме- тод Seek() используется для установки позиции в потоке. Он обеспечивает произ- вольный доступ к потоку и обычно используется при модификации или чтении определенного его содержимого. Метод Seek() принимает значение long и значение из перечисления SeekOrigin. Значение long задает смещение от базовой точки, ука- занной значением SeekOrigin, которое может быть Begin, Current или End, т. е. нача-
Потоки в .NET _ 45 лом потока, текущей позицией или концом потока соответственно. (Фактически метод Seek() используется и при установке свойства Position.) Методы чтения и записи для потоков делятся на две категории — соответствую- щие наборы методов существуют для синхронных и асинхронных операций. Имеются следующие синхронные методы: Метод Описание Read() и ReadByteO Эти методы используются для выполнения синхронного чтения из потока. Метод Read() считывает указанное число байтов и продвигает позицию в потоке вперед на число считанных байтов, а метод ReadByte() считывает из потока один байт и продвигает текущую позицию вперед на один байт. - Заметим, что в конце потока метод Read() возвращает 0, в то время как метод ReadByteO возвращает-1. Write() и WriteByteO Используются для выполнения синхронной записи в поток. Метод Write() записывает в поток последовательность байтов и продвигает текущую позицию в потоке вперед на число записанных байтов, а метод W г it eByte () записывает в текущую позицию потока один байт, продвигая позицию на 1. А вот асинхронные методы: Метод Описание BeginReadO и BeginwriteO С помощью методов BeginReadO и BeginWrite() можно выполнить асинхронные операции ввода/вывода. Оба метода принимают пять параметров: буфер в виде массива байтов, в который данные считываются или из которого они записываются, целочисленное смещение, указывающее на начальную позицию чтения или записи данных, целочисленный счетчик, задающий максимальное число считываемых или записываемых байтов. Четвертый параметр — это необязательный “делегат” AsyncCallback, вызываемый при завершении операции чтения или записи. Пятый и последний параметр — обеспеченный пользователем объект, с помощью которого он может отличить данный конкретный запрос от других запросов. - Оба метода возвращают интерфейс lAsyncResult, представляющий состояние асинхронной операции. EndReadO и EndWriteO Эти методы используются для завершения асинхронных операций ввода/вывода, они позволяют дождаться окончания асинхронных операций.
46 Глава 2 Рассмотрим методы управления потоком: Метод Описание Flush() Метод FlushO чистит все буферы И перемещает информацию в пункт назначения в зависимости от состояния объекта St ream. Close() Этот метод используется для освобождения ресурсов, связанных с потоком, например описателей файлов или сокетов. Он автоматически сбрасывает и сохраняет данные, поэтому вызывать метод Flush() перед методом Close () не требуется. Базовый механизм закрытия потока различается для каждого типа потока — в классе FileSt ream он освобождает все ресурсы файла, а в классе NetworkSt ream закрывает соответствующий сокет. Рекомендуется помещать вызов метода CloseO в блок finally, чтобы гарантировать закрытие потока при любом возникающем исключении. Set Lengt h() Этот метод используется для установки длины текущего потока. Заметьте, что если указанное значение меньше текущей длины потока, то поток обрезается. Если указанное значение больше текущей длины потока, поток увеличивается в размере. Чтобы можно было пользоваться методом SetLengt h (), поток должен поддерживать операции записи и поиска — в этом можно убедиться с помощью свойств CanWrite и CanSeek. При рассмотрении классов, производных от класса St ream, не будем обсуждать эти методы, если только они не приобретают специфический для данного типа по- тока смысл. Первым примером производного класса будет класс FileSt ream. Узнав, как реали- зовать только что рассмотренные члены в классе FileSt ream, без особого труда мож- но понять, как они применяются в других классах, производных от класса St ream. Класс FileStream Класс FileSt ream удобен для выполнения операций ввода/вывода для файлов, и, по существу, он является одним из наиболее важных и широко используемых клас- сов, производных от класса St ream. С помощью класса FileSt ream выполняются операции не только с файлами, но и с описателями системных файлов, например со стандартными устройствами ввода и вывода. Этот класс используется совместно с другими производными классами для создания временных файлов — например, можно сериализовать объекты в двоич- ный файл, а затем в любой момент преобразовать их обратно. При передаче файла по сети на стороне сервера с помощью этого класса возможно записать содержимое файла в поток, а на клиентской стороне воссоздать снова и сохранить полученный поток в формате исходного файла. Конструктор FileStream допускает несколько способов создания объекта FileStream, но все они включают задание или строки для пути файла, или описателя файла, который может использоваться для физического устройства, поддерживаю- щего потоки. Создание экземпляра FileStream через путь файла Далее описываются методы создания экземпляра класса FileStream через зада- ние пути файла. При создании потока ему по умолчанию выделяется буфер длиной
Потоки в .NET 47 8192 байта. Эту длину можно изменять перегрузкой конструктора, как будет рас- смотрено далее. Указание пути и режима файла Задав строку, представляющую путь к файлу, и значение из перечисления FileMode, можно создать объект FileSt ream. Параметр FileMode описывает, как откры- вать заданный файл; его значения приведены в таблице: Значение FileMode Описание Append Открывает существующий или создает новый файл для добавления данных. Этот файл не используется для чтения, поскольку указатель установлен в конце файла. Create CreateNew Создает новый файл, перезаписывая уже существующий файл. Создает новый файл, порождая исключение, если этот файл уже существует. Open Открывает существующий файл, порождая исключение, если файл не существует. OpenOrCreate Если файл существует, он открывается, иначе создается новый файл. T runcate Открывает файл и удаляет его содержимое, устанавливая указатель файла на его начало. Ткким образом, чтобы построить объект FileStream, создающий новый файл С:\Networking\MySt rearti. txt, нужно написать: // Использование пути файла и режима файла FileStream inF = new FileStreamC'C:\\Networking\\MyStream.txt", FileMode. CreateNew); Указание доступа к файлу Можно создать экземпляр класса FileStream, дополнительно указав параметры доступа к файлу из перечисления FileAccess. Это перечисление позволяет ограни- чивать пользователя определенными операциями с потоком. Значение FileAccess Описание Read Разрешается доступ к файлу только на чтение Write Разрешается доступ к файлу только на запись. ReadWrite Разрешается как чтение файла, так и запись в файл. Для проверки назначенных файлу разрешений FileAccess можно использовать рассмотренные ранее свойства CanRead и CanWrite. Итак, чтобы построить объект FileStream, который создает новый файл C:\Networking\MyStream.txt с доступом только на запись, надо использовать сле- дующее:
48 . > Глава 2 \ // Использование пути файла, режима файла и доступа к файлу Filestream inF s new FileStream("C:\\Networking\\MyStream.txt", FileMode.CreateNew, FileAccess.Write); Задание разрешений совместного использования Добавлением разрешения совместного использования контролируется доступ других процессов К потоковому объекту. Это полезно, когда файл совместно исполь- зуется двумя и более процессами. Разрешения совместного использования оп- ределяются значением из перечисления FileShare, которое представляет разные режимы доступа. Значение AleShare Описание Inheritable Порожденный процесс может наследовать описатель файла. Не поддерживается в Win32. None Ни один процесс (включая текущий) не может получить доступ к файлу— таким образом, файл нельзя совместно использовать. Read Дает текущему процессу и другим процессам разрешение только на чтение. Write Дает текущему процессу и другим процессам разрешение только на запись. ReadWrite Предоставляет текущему процессу и другим процессам разрешение на чтение и запись. // Задание пути, режима, доступа и разрешений совместного использования // Открывает файл на запись, другие процессы смогут только читать файл FileStream inF = new FileStream( “С: \\NetworkingWMyStream.txt", FileMode.Open, FileAccess.Write, FileShare.Read); Задание размера буфера Также можно создать экземпляр класса FileStream, указав в Дополнение к рас- смотренным параметрам размер буфера. В следующем примере зададим размер бу- фера, равный 1000 байтов: И Использование пути, режима, доступа, разрешений и размера буфера FileStream outF = new FileStream(“C:\\Networking\\MyStream.txt", FileMode.Open, FileAccess.Write, FileShare.Read, 1000); Задание синхронного или асинхронного состояния В следующем булевом значении можно задать, использовать ли для потока син- хронные или асинхронные оп<ерации ввбда/выводазначение t rue означает асин- хронные операции. // Использование пути, режима, доступа, разрешений, // размера буфера и асинхронных операций FileStream outF = new FileStreamt(“C:\\Networking\\MyStream.txtM, FileMode.Open, FileAccess.Write, FileShare.Read, 1000, true); Вместо того чтобы указывать путь к файлу, при создании объекта FileSt геатмож- но задавать описатель файла — уникальный идентификатор, который операцион-
noTOKiiB.NET _______________. 49 ная система назначает файлу при его открытии или создании. Описатель файла задается структурой IntPtr, которая представляет целое число специфичной для платформы длины. Для создания объекта FileSt ream с помощью описателя файла требуется указать по крайней мере описатель и FileAccess. Последующая перегрузка позволяет опре- делить принадлежность потока-- значение true указывает, что экземпляр FileSt ream получает монопольное использование. Следующими параметрами можно опреде- лить размер буфера (поумолчанию размер буфера по-прежнему равен 8192 байтам). Получить описатель файла для объекта FileSt ream можно, используя его свойст- во Handle: \ // Создание экземпляра FileStream FileStream inF = new FileStream(“С: \\NetworkingWMyStream.txt”, FileMode.Open); 11 Получение описателя файла IntPtr fHandle = inF.Handle; Чтение и запись в классе FileStream Мы потратили достаточно много времени на обсуждение методов и свойств классов, производных от класса St ream, поэтому теперь сразу перейдем к чтению и записи для FileStream. Рассмотрим синхронные и асинхронные режимы работы. Синхронный ввод/вывод Для выполнения синхронных операций чтения и записи потока класс Stream предоставляет методы Read() и Write(). Следующий пример выполняет синхронные операции ввода/вывода с файлом. В нем также используется метод Seek(), чтобы установить позицию в потоке. Сначала добавим пространство имен System. 10 для операций ввода/вывода и Пространство имен System.Text для методов, преобразующих строки в массивы байтов, — о них будет рассказано позднее в этой главе. using System; using System IQ; f using System.Text; class SyncIO Si public static void Main(string(J args) Создадим экземпляр FileStream, задавая для него FileMode как OpenOrCreate. Этот режим откроет файл, если он существует, или создаст новый файл. Когда впервые запускается этот пример, в той же папке, где находится исполнимый файл, создает- ся файл SyncDemo.txt. § // Создаем экземпляр FileStream ‘ FileStream syncF = new FileSt ream("SyncDemo. txt", FileMode.OpenOrCreate); Теперь можно приступить к исследованию разных синхронных методов. Нач- нем с метода WriteByte(). В этом примере символ преобразуется в байт и записы- вается методом WriteByte(). После записи байта позиция в файле автоматически увеличивается на единицу. X'Z syncF/WriteByte(ConyeitWfiyter^,l)^^ _
50 Глава 2 При использовании метода Write() в файл можно записать более одного сим- вола. Здесь мы преобразуем строку в массив байтов (методом GetBytesO класса Encoding из пространства имен System. Text) и затем методом Write() записываем этот массив байтов в файл. Метод W гite() принимает три параметра: записываемый массив байтов, позиция или смещение в массиве, с которого начинается запись, и длина записываемых данных. В данном случае записывается весь массив байтов с самого начала: Console. WriteLine(“-Write method demo byte[] writeBytes = Encoding.ASCII.GetBytes(" is a first character.”); syncF. Write(writeBytes, 0, writeBytes.Length); Итак, мы записали один байт методом WriteByte() и целую строку (преобразован- ную в массив байтов) методом Write (). Теперь считаем эту информацию обратно со- ответствующими методами Read(). Когда выполняются операции чтения и записи для объекта FileStream, текущая позиция (или указатель) в файле автоматически увеличивается на число считанных или записанных байтов. Мы хотим считать только что записанные байты, поэтому нужно воспользоваться методом Seek () для установки текущей позиции в потоке опять на его начало: // Устанавливаем указатель на начало * syncF.Seek(0, SeekOrigin.Begin); Теперь можно читать—в первую очередь считаем один байт методом ReadBy te (). Байт считывается из потока (с автоматическим увеличением позиции на единицу) и преобразуется обратно в символ: Console WriteLinef'-Readbyte method demo-”); t- // Считываем и отображаем байт Console.WriteLine(“First character is ->,* + Convert.ToCnar(syncF.ReadByte())); Далее считываем остаток файла методом Read(). Этот метод принимает три па- раметра: массив байтов, в котором будут сохранены считанные байты, позицию или смещение в массиве, откуда начнется чтение, и число считываемых байтов. Здесь считаем из файла все оставшиеся байты, но поскольку мы находимся в позиции 1 файла, нужно считать syncF. Length — 1 байт: // Используем метод Read Console.WriteLine (’‘—Read method demo—”); // Выделяем буфер bytefl readBuf * new byte[syncF.Length-1]; // Читаем файл :|T syncF Read(readBuf, 0, (Convert.Tolnt32(syncF.Length))-!) ; Считанный массив данных преобразуется в строку методом GetStringQ класса Encoding и отображается на консоли: Ж k //Отображаем содержимое . ' Console,Writetine(“The rest of the file is a ” + Encoding ASCII.GetString(readBuf)) , Л «fc « 'JF vw
Потоки в .NET 51 Этот код дает следующий вывод: Асинхронный ввод/вывод Один из перегружаемых конструкторов класса FileStream предоставляет флаг IsAsync, определяющий синхронное или асинхронное состояние. Объект FileSt ream открывается асинхронно, если передавать в этом флаге значение t rue. Отметим, что операционная система должна поддерживать асинхронный ввод/вывод, иначе операции будут синхронными — Windows NT, 2000 и ХР поддерживают как синхрон- ный, так и асинхронный ввод/вывод. Асинхронный ввод/вывод немного сложнее, чем синхронный. Здесь мы лишь в общих чертах наметим, как можно реализовать асинхронный ввод/вывод. Более глубоко эта тема будет рассмотрена в главе 4. Чтобы реализовать асинхронный ввод/вывод, требуется использовать специ- альный механизм обратного вызова, и делегат AsyncCallback дает для приложе- ний-клиентов способ реализации этого механизма. Делегат обратного вызова передается методу BeginRead() или BeginWrite(). Рассмотрим пример асинхронного чтения. Начнем с обычных пространств имен и нескольких статических полей для сохранения объекта FileSt ream и массива байтов: using System; using System.10; using System.Text; using System.Threading; public class AsyncDemo ! { s И // Объект Stream для чтения static FileStream fileStm:... // Буфер для чтения Static bytefl readout f Объявляем поле-делегат AsyncCallback для функции обратного вызова: // Делегат Async-Call-back 3 static AsyncCallback Callback; В методе Main() инициализируем поле-делегат обратного вызова, чтобы оно ука- зывало на метод CallBackFunction(), который будет вызываться при поступлении сигнала о завершении асинхронной операции чтения. Подробнее об этом процессе рассказывается в главе 4. public static void Main(String[] args) Г J < ' .. 7, Л . • . ' > ' $ - Callback = new AsyncCallback(CallBackFunctiori);
52 Глава 2 Теперь можно инициализировать объект FileSt ream, задав для асинхронных опе- раций последнее значение true: fileStm = new FileStream(@"C:\Networking\Streams\Test.txt", FileMode. Open, FileAccess. Read, ' *.• FileShare.Read, 64, true); readBuf <= new byteffileStm. length); Для инициирования асинхронных операций чтения потока используем метод Beg inRead(). Делегат обратного вызова передается методу BeginRead() в предпослед- нем параметре. // Вызываем асинхронное чтение - * * fileStm.BeginRead(readBuf, 0, readBuf.Length, Callback, null);-' Данные будут считываться из потока FileSt ream, а мы в это время можем продол- жить другую работу — здесь мы делаем вид, что заняты другой работой, просто вы- полняя цикл и время от времени переводя основной программный поток в режим ожидания. Как только цикл завершается, FileStream закрывается. // Имитация основной обработки for (long i - 0; i < 5000; i++) (i % woo == o) if { Console.WriteLine(“Executing th Main - " + i.ToStringf)); Thread.Sleep(iO); FileStm.Close(); Заметим, что если цикл закончится до завершения считывания, поток FileSt ream все равно будет закрыт — в нашей программе основной поток не ждет, пока Вед 1 nRead () завершит работу. В главе 4 рассмотрим пример с ожиданием завершения асинхронной операции перед выполнением следующих действий — это может быть существенно, если надо выполнять асинхронные операции в каком-то особом по- рядке или рассчитывать на то, что одна операция будет завершена до начала следую- щей. Функция обратного вызова, соответственно названная здесь CallBackFunction(), вызывается, когда буфер заполнен. Для завершения асинхронного чтения вызывает- ся метод EndRead(). Если файл не закончен, метод BeginReadO вызывается снова для продолжения чтения, а последние считанные данные отображаются на консоли: static void CallBackFunction(IAsyncResult asyncResult) /7 Вызывается, когда буфер заполнен. int readB = fileStm.EndRead(asyncResult) ; if (readB > 0) • '<• tb 1' ''' " ' ' > ’ i I << , fileStm.BeginRead(readBuf, 0, readBuf Length, Callback. Console.W riteLine(Encoding ASCII.GetSt ring(reaoBuf, 0, readB)j• null); } 1
Потоки в .NET 53 И вот типичный вывод этого примера — скорость процесса, размер файла и бу- фера могут иметь другие значения: Класс BufferedStream Буфер — это зарезервированная область памяти, используемая для сохранения временных данных, его главное назначение состоит в повышении производитель- ности ввода/вывода, и он часто используется для синхронизации передачи данных между устройствами, имеющими разные скорости. Многие мультимедийные прило- жения реального времени используют буферы как промежуточную память. А такие устройства, как принтеры, снабжены собственными буферами для хранения данных. В .NET можно реализовать буферизацию с помощью класса BufferedStream. Объекты класса Ви f feredSt ream заключают в оболочку еще один объект Stream. Обыч- но для хранения данных в памяти BufferedStream используется с Networkstream. Объекты FileStream уже имеют собственный внутренний буфер, а объекты Memory- Stream буферизации не требуют. Когда создается буфер с помощью класса Buf feredSt ream, то по умолчанию выде- ляется буфер с размером 4096 байтов, но через один из многочисленных перегружа- емых конструкторов можно задать другой размер. В следующем примере показан метод чтения буферизованного потока. Этот ме- тод принимает объектный параметр Stream, “завертывает” его в объект Buffe- redStream и выполняет операцию чтения. Таким же способом выполняются другие операции с объектом BufferedStream. // Чтение BufferedStream , г > "Д < <’« 1 public’static void readBufStre,am(Stream st) ? // Формируем BufferedStream: й BufferedStream bf » new BufferedStream!st) ; byte[] inData « new Byte (st. Length]; „ «. x’. ’ ’ ’ ,, •• | // Читаем и отображаем буферизованные данные bf.Read(inData, 0, Convert,iolnt32(st.Length)); < Г ' ' 41 . ... . Console.Writeline(Encoding.ASCII.GetString(inData)); Класс Memorystream Бывают ситуации, когда приложениям часто требуются данные, например, по- иск справочных данных в таблице. Тогда сохранение данных в файле может привес- ти к задержкам и снизить производительность приложения. Решение для таких случаев предоставляет класс Memorystream, с помощью которого данные можно со- хранять в памяти. Класс Memorystream полезен для быстрого временного сохранения данных. Хоро- ший пример его применения—передача сериализованных объектов внутри процес-
54 Глава 2 ca, можно использовать MemorySt ream для временного сохранения сериализованных объектов. Такой подход улучшает производительность по сравнению с использова- нием классов FileStream или BufferedStream. Создание объекта Memorystream довольно сильно отличается от создания объек- тов FileSt ream и BufferedStream. Экземпляр этого класса можно создать несколькими способами. В следующем примере показывается, как создавать и использовать MemoryStre- am - используется метод WriteToQ, чтобы записать весь поток из памяти в файл. using System; using System ДО; using System,Text ; public class memStreamDemoCJass { public static vpid Main(String[J args) { Экземпляр класса MemoryStream создается без передачи параметров. Чтение и за- пись данных для MemoryStream выполняются точно так же, как в примере с классом FileStream. Здесь используется метод Write(), чтобы записать простую строку. // Создаем пустой поток в памяти MemoryStream mS = new MemoryStream(); * byte[] memData = Encoding; ASCII. Get Bytes ("This will gp inM^hp.'/I!.”); // Записываем данные mS.Write(memData, 0, memData.Length); После записи строки считаем ее, используя метод Read(). Заметьте, что перед чтением текущая позиция устанавливается в нуль. // Устанавливаем указатель на начало mS.Position - 0; byte[] inData = new byte[100); // иитаек из памяти mS Read(inData, 0, 100); Console.Wri teLlne(Encoding. ASCII. GetString(ir.Data)); г Метод WriteToO используется для записи всего содержимого потока в памяти в файловый поток. Stream strm = new FileStream(“CA\NetworKing\\Streams\\Mem0utou1 txt", FileMode. OpenOrC r ea te,- FileAccess.Write): mS.Writelb(strm); * ::1 4= : J Класс Networkstream Данные передаются по сети между двумя узлами в форме непрерывного потока. Для обработки таких потоков в платформе .NET имеется специальный класс Net- workStream, который входит в пространство имен System. Net. Sockets и используется для отправки и получения данных через сетевые сокеты. Объект Networkstream — это небуферизированный поток, который не поддержи- вает произвольный доступ к данным. Невозможно изменить позицию внутри пото- ка, и, следовательно, использование метода Seek() и свойства Position порождает
Потоки в .NET 55 исключение. Свойство CanSeek для объекта Networkstream всегда возвращает значе- ние false. Поскольку класс NetworkSt ream не имеет буфера, с ним вместе обычно используете ся BufferedStream, играющий роль среды промежуточного хранения. Далее описываются некоторые важные члены класса Networkstream: Свойство Описание DataAvailable Возвращает логическое значение, указывающее, доступны ли данные в потоке для чтения. Значение true указывает, что данные в потоке доступны. Readable Используется для получения или установки булевого значения, указывающего разрешен ли доступ на чтение потока. Это свойство работает так же, как свойство CanRead в других потоках. Socket Возвращает базовый объект Socket. Writeable Используется для проверки, можно ли записывать данные в этот поток. Значение t rue означает, что запись в поток разрешена. Свойство работает так же, как свойство CanWrite в других потоках. Каждый конструктор Networkstream требует по крайней мере задать объект Socket, а дополнительно можно указать принадлежность потока и/или значение из перечисления FileAccess, которое, как было показано ранее, контролирует разре- шение чтения и записи. Установка значения true в флаге принадлежности дает объекту NetworkSt ream контроль над сокетом, и при использовании метода Close () за- крывается базовый сокет. Объект Networkstream можно также получить от класса TcpClient. Мы посвятим TCP всю главу 5, но затрагиваем эту тему и здесь, чтобы проиллюстрировать Net- workStream в сценарии клиент-сервер. Метод TcpClient. GetStream() создает объект NetworkSt ream, передавая его базовый сокет как параметр конструктора. Рассмотрим код для простого TCP-слушателя с использованием Networkstream. Классы TCP можно найти в пространстве имен System. Net Sockets. using System; using System. ID; using System.Text; using System.Net;- »- using System. Net. Sockets; class TCPListenerDemo { f public static void MainO { try Первое, что мы сделаем, создадим объект TcpListener, ожидающий поступление данных на порте 5001, и начнем слушать с помощью метода Start (). Метод Accept- TcpClient()принимает запрос на соединение, возвращая объект TcpClient. Объект Networkstream создаем, используя метод GetStream(),: ’ // Создаем ТС^слушатеЛя^Ч^'^ , % TcpListener listener = new TcpListener(5001); , fen. ж .ListenefeStaJEtXAw»,.,..?1. w._
56 Глава 2 TcpClient tc - listener.AcceptTcpClientO1; Networkstream stm « tc. GetSt reamO; Теперь можно читать данные, используя метод Read(): bytef] readBuf * new byte[lOOJ; stm.Read(readBuf, 0, 100); // Отображаем данные Console.WriteLine(Encoding.ASCII.GetSt ring(readBuf)); stm.CloseO; } catch (Exception e) { Console.WriteLine(e.ToSt ring()) .; } } Для использования этого примера нужно, чтобы было приложение-клиент, по- сылающее какие-либо данные. Не забудьте до приложения-клиента запустить при- ложение-слушатель! Вот код для ТСР-клиента: using System; using System.IQ- using System.Text; using System Net; using System.Net.Sockets; class TcpClientExample { static void Main(string[} args) { try { Мы создаем объект TcpClient и соединяем его с localhost на порте 5001. Еще раз используем метод GetStream(), чтобы вернуть базовый объект Networkstream: // Создаем ТСР-клиент TcpClient client = new TcpClientO; // Устанавливаем соединение, используя имя хоста и порт client.Connect (‘Tocalhost”, 5001); а // Получаем экземпляр Networkstream для отправки данных Networkstream sto = client.GetStream(); Теперь есть объект Networkstream, и отправка данных — это тот же самый про- цесс, который мы использовали с другими потоками: byte[]sendBytes ® Encoding.ASCII.GetBytes(“This data has come from" + “ another place!! 1 stm.Write (sendBytes. 0, sendBytes.Length); Наконец, закрываем TcpClient: client.CloseO; j . } ; - 'W catch (Exception e) { . Console.WriteLine(e.ToStringO); \ Console.WriteLine(“The listener has probably not started’’);
Потоки в .NET 57 J } } В следующих главах рассматриваются примеры с созданием разных по сложнос- ти приложений “клиент-сервер”. Класс CryptoStream Для определенных типов данных их защита при передаче и хранении является очень важным требованием. Для защиты данных, как правило, применяется их шифрование секретным клю- чом. В зависимости от алгоритма для дешифрования может использоваться тот же секретный ключ, что и для шифрования, или другой ключ в зависимости от алгорит- ма шифрования. Платформа .NET предоставляет класс CryptoStream, связывающий потоки с криптографическими преобразованиями. Хотя CryptoStream не принадлежит на самом деле пространству имен System. 10, он все же порожден от класса St ream Класс CryptoStream может использоваться для выполнения криптографических операций на объекте St ream. Конструктор CryptoStream принимает три параметра: первый задает используе- мый поток, второй — криптографическое преобразование, а в третьем указывается доступ на чтение или запись к криптографическому потоку. В нашем распоряжении имеются самые разные криптографические преобра- зования — можно использовать любой провайдер службы криптографии, реали- зующий интерфейс ICryptoTransform. В следующем примере демонстрируется использование нескольких криптографических провайдеров — они находятся в пространстве имен System. Security. Cryptography. using System; using System.10; using System.Text ; using System.Security.Cryptography; A public class crypt V { public static void Main() { В первую очередь предлагаем пользователю выбрать сервисный провайдер. Все сервисные провайдеры порождены от класса SymmetricAlgorithm, имеющего один секретный ключ, который используется и для шифрования, и для дешифрования. Console.WriteLine(“Select Service Provider for CryptoStream”); Console. WriteLine(“1 = DESCryptpServiceProvider”);? Console.WriteLine(“2 - RC2CryptoServiceProvider”); Console.WriteLine("3 = RijndaelManaged”); Console.WriteLineC“4 = TripleDESCryptoSe rviceProvide r”); Console. WriteLine( “5 = SymmetricAlgorithm”).; // Создаем объект des SymmetricAlgorithm des = pull; switch (Console. ReadlineO) j case “1” : ... ? -a des = new DESCfyptoServiceProvider(>;
58 Глава 2 UdoC <v 7':r' ' •-' ’:Лл •• ‘ des s new RC2CryptoServiceProvider(); break; % ' • * • case “3”: des s new RijndaelManagedO ; jL break; , case “4"r - ’ ’ des = new TripleDESCryptoServiceProviderO; break; ease “5" : des * SymmetricAlgorithm Create(); // используется алгоритм no умолчанию break;, default; Console.WriteLlne ("Wrong selection”!; return; ' 4 Для сохранения шифрованных данных создается объект FileSt ream, который мы заключим в оболочку объекта CryptoStream. Интерфейс ICryptoTransform помогает определить основные операции криптографического преобразования, которое со- здается с использованием метода CreateEncryptor класса Symmet ricAlgorithm. FileStream fs = new FileStream(“SecretFile.dat”, FileMode.Create, FileAccess.Write) ; ICryptoTransform desencrypt = des.CreateEncryptor(); CryptoStream cryptostream = new CryptoStream(fs, desencrypt, CryptoStreamMode. Write); Теперь зашифруем наше сообщение. Будем использовать простую строку, кото- рую преобразуем в массив байтов с помощью метода GetBytes() класса Encoding из пространства имен System.Text (подробнее о кодировании рассказывается далее). Получив массив байтов, записываем его в CryptoSt ream методом Writе(). string theMessage = “A top secret message”; f •r. byte[] bytearrayinput = Encoding.Unicode GetBytes(theMessage); t Console.WriteLine(“Original Message : 0”, theMessage); cryptostream.Write(bytearrayinput, 0, bytearrayinput Length); cryptost ream.Close($; f s-. Close(); Закрыв потоки, мы можем приступить к дешифрованию сообщения — во второй части нашего кода зашифрованное сообщение считывается из файла и затем преоб- разуется обратно к исходному виду. /***»**»★*** Этап дешифрования ..*•***•******/ // Создаем поток в файле для чтения зашифрованного файла FileStream fsread = new FileStream(“SecretFile.dat”, FileMode.Open, FileAccess.ReadWrite); byte[] encByte = new byte[fsread.Length]; fsread.Read(encByte, 0, encByte. Length.) ; Мы определили размер массива байтов, воспользовавшись свойством Length объекта FileStream, позволяющего узнать длину данных, записанных в файл. Мы считываем данные в этот массив методом Read (). Перед дешифрованием отобразим
Потоки в .NET 59 зашифрованное сообщение на консоли и установим в нуль текущую позицию в объекте FileSt ream. Console.WriteLine ("Encrypted Message + Encoding.ASCII.GetStriog(encByte)); fsread.Position - 0; При дешифровании данных используется процедура, аналогичная шифрова- нию. Главное отличие — в применении метода CreateDecryptor(), создающего задан- ный объект decryptor. Создадим новый массив байтов, в который считаем данные из CryptoStream, азатем воспользуемся методом GetSt ring() класса Encoding, чтобы пре- вратить массив байтов в отображаемую строку. ?? // Из экземпляра des создаем DES Decryptor ICryptoTransform desdecrypt = des. С reate Decryptor О'; K CryptoStream cryptostreamDecr = new CryptoStream(fsread, desdecrypt, CryptoSt reamMode.Read); byte[] decrByte = new byte[fsread.Length]; cryptostreamDecr'. Read(decrByte.,. 0, <int)fsread.Length); string output = Encoding Unicode.GetString(decrByte); Console.WriteLine(“Decrypted Message : 0" ,output); cryptostreamDecr .,Close(); fsread.Close(); л • > - } На следующем снимке экрана показан вывод из приведенного выше кода после двух его запусков с методом шифрования Symmetric Algorithm: Обработка потоков Мы рассмотрели разные типы потоков, узнали, как создавать потоки, считывать и записывать данные. Однако смогли считывать и записывать только массивы бай- тов, а это несколько неуклюжий способ работы с данными. Например, если понадо- билось бы записать в файл какие-нибудь десятичные значения, нам пришлось бы самим разбирать их на байты—но пространство имен System. 10 предоставляет клас- сы и методы для обработки различных типов данных в потоках.
60 Глава 2 В этом разделе рассмотрим следующие классы, позволяющие обрабатывать по- токи: □ Обработка двоичных файлов в классах BinaryReader и BinaryWriter □ Обработка текстовых файлов в классах StreamReader и StreamWriter Прежде чем обратиться к этим классам, нужно рассмотреть кодирование — ту возможность, на которую мы ссылались ранее в этой главе, когда выполняли преоб- разования массивов байтов для пересылки данных из потоков. Кодирование строковых данных Хотя данные, переносимые через потоки, представлены в байтовой форме, за- ключенная в них информация бессмысленна, если мы не знаем, что означают эти байты. Например, если байты нужно преобразовать в символы, то нужно знать, как установить соответствие между этими байтами и символами, потому что некоторый тип символов может потребовать более одного байта. • Класс Encoding пространства имен System. Text предназначен для выполнения по- добных операций. Класс Encoding обрабатывает наборы символов Unicode, рас- пространенного во всем мире стандарта кодирования символов, который делает возможным универсальный обмен данными и улучшает возможности обработки многоязычного текста. Многие языки, например японский, не могут быть представ- лены без Unicode. Расширенный набор символов, поддерживаемый форматом Unicode, означает, что информация, закодированная с помощью Unicode, обычно требует 16-битового пространства вместо стандартных 8-битовых строк. В среде .NET Framework имеется несколько классов, производных от класса Encoding, позволяющих перекодировать разные форматы. Класс Использование ASCIIEncoding Кодирует символы Unicode как одиночные однобайтовые символы ASCII. Эта кодировка имеет ограничение, поскольку поддерживает 7-битовые символьные значения и нВ является удачным выбором для приложений, поддерживающих обработку многоязычных текстов. UnicodeEncoding Кодирует каждый символ Unicode в двух байтах UTF7Encoding Перекодирует в 7-битовую кодировку Unicode. - UTF (Unicode Translation Format) 7 и 8 — широко распространенные форматы, используемые для передачи по сети данных, базирующихся на Unicode. UTFSEncoding Этот класс поддерживает 8-битовую кодировку UTF-8, широко используемую приложениями, поддерживающими обработку многоязычных текстов. - По умолчанию UTF-8 используется в XML.
Потоки в .NET 61 Ниже приводятся некоторые основные методы класса Encoding; общая цель этих методов заключается в выполнении преобразований между массивами байтов и массивами'Символов или строками: Метод Назначение GetCharCountO Этот метод получает массив байтов и возвращает подсчитанное число символов, полученных при декодировании. GetCharsO Декодирует массив байтов в массив символов. - Этот метод можно использовать разными способами в зависимости от перегрузки. Каждый перегружаемый метод принимает массив байтов и декодирует его в массив символов. GetByteCount() Число байтов, необходимых для кодирования массива символов Перегружаемые методы могут принимать либо массив символов, либо строку и возвращают число байтов, необходимых для выполнения перекодирования. GetBytesO Перекодирует строку или массив символов в массив байтов. GetDecoderO Получает объект Decoder, представляющий абстрактный класс, используемый для преобразования байтов в символы Unicode. - Этот метод возвращает объект Decoder, который можно использовать для декодирования последовательности байтов в символы. GetEncoderO Получает объект Encoder, представляющий абстрактный класс, используемый для преобразования символов Unicode в массив байтов. - Этот метод возвращает объект Encoder, который можно использовать для перекодирования последовательности символов в байты. Л Рассмотрим простой пример, преобразующий строку в массив байтов с исполь- зованием различных форматов и отображающий значения из массива байтов: using System; using System 10; -S using System. Text;; fefcr , • . * ла class Encodingtest public static void Main(string[] args) W'; .-/L* У " 1 • ; string test = “This is our test string. byte[] asco; byte[] unicb, bytel] utfb; Мы создали три массива байтов. Преобразуем нашу тестовую строку в массивы, используя кодировки ASCII, Unicode и UTF7. После каждого преобразования метод DisplayAr гау() будет выводить на консоль все байты в байтовый массив:
62 Глава 2 ascb = Encoding.ASCII.GetBytes(test); Console.WriteLine(“ASCII Encoding Gbytes”, ascb.length); OisplayAf ray(ascb); unicb = Encoding.Unicode.GetBytes(test); Console.WriteLine(“Unicode Encoding ; 0 bytes’’,, unicb.Length); OisplayArray(unicb); utfb « Encoding,UTF7.GetBytes(test); Console.WriteLine(“UTF Encoding : 0 bytes", utfb.Length); DisplayArray(utfb); После того как массивы байтов отображены, посмотрим, какой эффект оказыва- ет обратное преобразование массива байтов в строку—для этих преобразований за- дадим другие кодировки, не те, которые использовали сначала: string unics = Encoding.Unicode GetString(ascb);, Console.WriteLine(unics); string ascs = Encoding.ASCII.GetString(unicb); Console WriteLine(ascs); ) static void OisplayArray(byte[] b) for (int i=0;i<b.Length;i++) Console. Write(b[i]+" “); Console. WriteLineO; } ? } На консоли мы получаем следующий вывод — заметьте, что каждый символ из строки после перекодировки в формат Unicode дает два байта. Заметьте также, что строки, полученные после обратного преобразования, иллюстрируют важность корректного кодирования на каждой стадии: Двоичные файлы Классы BinaryReader и Binarywriter из пространства имен System. 10 используют- ся для работы с простыми типами данных из потоков. Каждый класс создается вок- руг существующего объекта St ream. . BinaryReader Класс BinaryReader используется, чтобы читать простые типы данных. По умол- чанию для чтения потока в нем используется кодировка UTF-8. При создании экзем- пляра этого класса вы можете указать нестандартную кодировку.
Потоки в .NET 63 У класса BinaryReader есть различные методы для чтения простых типов данных. Метод Read () считывает байты из потока и продвигает вперед позицию в потоке, возвращая -1, если достигнут конец потока. Два других перегружаемых метода по- зволяют считывать данные в массив байтов или в массив символов, указывая началь- ную позиций) в потоке и число считываемых байтов. Для чтения без продвижения позиции в потоке служит метод PeekChar(), возвращающий следующий символ из по- тока или -1, если мы достигли конца потока или поток не поддерживает поиск. Для каждого простого типа данных существует метод, предназначенный для чте- ния данных этого типа из потока и продвижения позиции в потоке в соответствии с длиной этого типа данных. Метод Число считываемых байтов bool ReadBoolean() 1 byteReadByteO 1 byte[] ReadBytes(int count) count char ReadCharO Зависит от используемой кодировки decimal ReadDecimalO 16 double ReadDoubleO 8 short Readlntl6() 2 int Readlnt32() 4 long Readlnt64() 8 float ReadSingleO 4 string ReadStringO Зависит от длины строки При чтении из потока его 'конец можно обнаружить с помощью метода Pee kCha г (), который читает из потока следующий символ без продвижения позиции. Если в потоке больше нет символов, возвращается значение -1. Метод Close() закрывает объект. Базовый поток объекта BinaryReader можно по- лучить из его свойства BaseSt ream. Экземпляр класса BinaryReader вы можете создать, передав конструктору поток с тутом кодировки или без него. // Создаем экземпляр Stream Stream strm = new FileStream(,‘C:\Networking\Streams\MyStream.txt,,I FileMode.Open, FileAccess.Read); // Используем экземпляр Stream для создания BinaryReader BinaryReader br = new BinaryReader(strm); При создании объекта В i na ryReade г вы также можете указать тип кодировки: // Создаем экземпляр Stream StreanrstHD = new FileStream(@"C:\Networking\Streams\Book.txt", FileMode.Open, FileAccess.Read);
64 Глава 2 // Используем экземпляр потока для создания объекта BinaryReader BinaryReader br = new BinaryReader(strm, Encoding.ASCII); Binarywriter Класс Bina ryWr ite г используется для записи в поток простых типов данных в дво- ичном формате. Запись в поток достигается с помощью метода Write (). Для записи в поток каждо- го простого типа данных существует свой перегружаемый метод, продвигающий по- зицию в потоке в соответствии с длиной типа данных. Чтобы перемещаться по потоку, используется метод Seek(), устанавливающий позицию в потоке, как метод Sqe к () класса St г е ат. На самом деле метод BinaryWriter.Seek() просто вызывает метод Seek() базового объекта Stream (который можно получить из свойства BaseStream). Также имеются методы Close() и Flush(), выполняющие обычное управление возможностями при записи. Пример чтения и записи двоичных данных В следующем примере объект FileStream создается для чтения и записи двоич- ных данных. В первой части кода данные записываются в файл, а во второй части эти же данные считываются и отображаются на дисплее. using System; л using System ДО/ class BinaryGetting static void Main(string[] args) double angle, sihAngle; FileStream fStream = new FileStrearn(©'C:\Networking\Streams\Sines.dat“, ' FileMode.Create, FileAccess.Write); Binarywriter bw = new BinaryWriter(fStream); Сначала мы создаем объект FileStream — файл с именем Sines.dat создается в папке C:\Networking\Streams, и указываем доступ на запись, поскольку будем запи- сывать в поток данные. Затем создаем объект BinaryWriter на основе объекта Fi 1 eSt ream. Мы будем вычислять синус углов от 0 до 90 градусов с интервалом 5 граду- сов (до вычисления синуса надо перевести в радианы заданный в градусах угол), а за- тем с помощью метода Write() записывать эти значения в поток. for tint 1-0;i<=90;i+=5) { double angleRads - Math.PI*i/180; SinAngle = Math.Sin(angleRads); bw.Write((double)!); bw.Write (sinAngle); bw CloseO ; f Stream. Closet); , Объекты BinaryWrite г и FileSt ream закрыты, и можно начать процесс извлечения данных объектом BinaryReader. Сначала создадим новые объекты FileStream и BinaryReader.
Потоки в ,NET 65 FileStream FrStream ₽ new File$tream(@T:\Networking\streams\Sines.dat", * / FileMode.Open, FileAccess.Read);; BinaryReader br = new BinaryReader(frStream).; endOfFile; wsaJ' * ” ..ч*'* Чтобы считать данные обратно, будем использовать метод ReadDouble(). Если этот метод попытается считать данные за концом потока, возникнет исключение, поэтому используем метод PeekChar() для обнаружения конца файла без продвиже- ния позиции: . .do . j^1 endOfFile = br.PeekChar(); if(endOfFile > -1) angle » br.ReadboubleO; s sinAngle = br ReadDoubleO; Console.WritelineC'O : 1‘\ angles sinAngle) 5- •% ; while(endOfFile!- -1); Наконец мы закрываем BinaryReader и базовый поток. br.Close(); frStream.Close() ; В результате получаем следующий вывод: C:\WINNT\sy$tem32\CMDXXE C:\H t"9i4.i.ig\St> в. n >readbinary 0 : О 5 : 0.0871SS7427476S82 10 : 0.1736-1817766693 15 : 0.258819045102521 20 : 0.312020143325669 25 : 0.422618261740699 30 : 0.5 35 : 0.573576436351046 40 : 0.642787609686539 45 : 0.70719b?81186547 50 : 0.766014443118978 55 : 0.819152044288992 60 : 0.866025403784439 65 : 0 90630778703665 70 : 0.939692620785908 75 : 0.965925826289068 80 : 0.981607753012208 85 : 0.996194698091746 90 : 1 TextReader TextReade г используется для чтения из потока текста или символов. Это абстрак- тный класс, от которого порождены классы StreamReader и StringReader. Текст, счи- танный объектом St ringReadeг, сохраняется в объекте St ringBuilde г, а не в обычном типе string.
66 Глава 2 Некоторые важные методы класса TextReader показаны в таблице: Метод Описание Peek() Возвращает следующий символ из потока или строки, не продвигая позицию. Read() , Считывает символы/байты из потока/строки ReadBlock() Считывает указанное число символов из текущего потока с заданной начальной точки. ReadLineO Считывает все символы вплоть до конца строки (обозначенного возвратом каретки и переводом строки). ReadToEndO Считывает все символы вплоть до конца потока/строки. Synch ronizedO Создает поточно-безопасную оболочку, чтобы несколько потоков могли использовать TextReader Close() Закрывает текущий объект reader Работа с классом StreamReader Совершенно очевидно, что StreamReader используется для чтения символов из потока байтов. Объект StreamReader использует кодировку (заданную в конструкто- ре) или, если она не задана явно, кодировку UTF-8. Этот объект, по существу, пред- оставляет только однонаправленный доступ к потоку в отличие от BinaryReader, допускающего произвольный доступ через метод Seek(). Имеется возможность из- менить позицию в базовом потоке, обратившись к нему через свойство BaseSt ream объекта StreamReader, однако текущая позиция в потоке и текущая позиция объекта reader из-за буферизации могут не совпадать. Ниже в примере демонстрируется использование объекта St reamReade г. Он счи- тывает текст из файла TextOut.txt, который мы действительно создадим в следую- щем примере с объектом St reamWrite г, но сейчас подойдет любой текстовый файл. using System; using System.10; class TextReadingExample ( - Й. I static void Main(sxring[ 1 args)4 Stream fS - new FileStream(“TextOut.txt”, FileMode.Open., FileAccess.Read) //.^Использование объекта Stream StreamReader sReader - new StreamReader(fS); string data; int line=0; Далее начинается основной цикл считывания строк. Для чтения строки из пото- ка используется метод Headline() - если в потоке больше нет строк, возвращается null. Поэтому мы включаем эту проверку в цикл while: Ч while ((data = sReader.ReadlineO) ! = null)
Потоки в .NET 67 Отображаем на экране следующую информацию: счетчик строк, который мы сами будем увеличивать, затем считанную строку и-наконец позицию в потоке: * 1 1 ? ' ••<», - V 'sis ' > Console. WriteLine ("Line 0: 1 r Position « 2", ++line, data, sReader.BaseStream.Position); У * З.ЙЙ ЗМ a.-» ’* '«& V Теперь установим позицию на начало потока и считаем все его содержимое ме- тодом ReadToEnd(): // Устанавливаем позицию» используя поисковое свойство базового потока sReader.BaseStream.Seek(O, SeekOrigin.Begin); i,!— Э Console.WriteLine(“* Reading entire file using ReadToEnc \n" + t • sReade r.ReadToEnd(j); sReader.Close() fS.Close(); } } Запустив приведенный выше код с файлом TextOut. txt, созданным в следующем примере, вы увидите такой результат: сч C:\WINNT\system32\cmd exe C:\Networking\Streans>StrcanRead Line 1 : Today is Wednesday. ~ Line " - " ‘ Line Line Line Line Line Line Line Line ________ ___________ _____ * Reading entire file using ReadToEnd Today is Wednesday. Toda > we will nor.tly be using StreanU EILl^. _ J , ".— .... .... i “ 2 3 4 S 6 CDEJI 267 Position “ 267 will nostly be using StreanWriter. : Position “ its square ’ ~ ~ •- • - — its square its square its square its square Today Value Value Value Value Value .._________________ ___________ _____ Arrays can be written : array : Position ° 267 And parts of arrays can be written : Position 1 267 ve 0. 2 3. 4 8 9 _________ ______._ _ 10 : arr : Position x 267 Position - 26? - 267 = 267 ° 267 4 9 ________ 16 : Position « 267 Position Posit ion в Интересный момент, который стоит отметить, касается позиции — ее значение не изменяется при чтении строк и равно 267, длине всего файла. Этот пример на- глядно иллюстрирует, как расходятся позиция потока и позиция в объекте reader. Содержимое файла помещается в буфер — в этом можно убедиться, если добавить в цикл while следующую строку: { Console.WriteLine(“Line 0:1: Position = 2", ++line, data, sReader.BaseSt ream Position); sReade r=. DiscardBuf feredData (); } Метод DiscardBuf fe redData() отбрасывает данные объекта reader. Если добавить эту строку и запустить код снова с тем же самым файлом, то будет отображена лишь одна строка, поскольку после вызова метода DiscardBuf feredDataO все оставшиеся строки отбрасываются. Textwriter Textwriter — это абстрактный класс, используемый для записи текста, и он выво- дит последовательные наборы символов. От класса Textwriter порождены классы Streamwriter и Stringwriter. Каки StrrngReader, класс St ringWriter имеет дело с объек- тами StringBuilder, а не с обычными строками.
68 Глава 2 Методы класса Textwriter позволяют управлять записью и выполняют саму запись: Метод Назначение Close() Закрывает текущий объект writer. Flush() Чистит буферы и записывает данные в базовое устройство. Synchronized() Создает поточно-безопасную оболочку. Write() Записывает указанные данные. WriteLineO Аналогично Console. WriteLine() этот метод записывает данные с признаком конца строки. Метод WriteLine() можно использовать несколькими способами в зависимости от переданных параметров. Работа с классом Stream writer Класс Streamwriter используется для записи символов в поток. По умолчанию в нем используется кодировка UTF-8. Существует несколько способов создания экземпляра класса Streamwriter в зави- симости от переопределяемых параметров, один из этих способов позволяет пере- дать в параметре объект Stream. Если вы используете объект FileStream, значит, вы имеете больший контроль над доступом к файлу, чем это возможно с другими конструкторами St reamW rite г. В следующем примере демонстрируются различные свойства класса Stream- Writer — после разбора примера их использование станет гораздо понятнее. using System; чзмд System.10; class TextWritingtxample .static voic Hain( string [J args); ( ? ' v & wf -а. л' ’ a Stream fS•-« new FileSt reamC'TexWuX.^txt-.FilfcMpde CraateNew, . File» ,cceM. Write)af * v. •1 * // Использование объекта Stream Streamwriter sWHter = new StreamWriter(fS); Начинаем с отображения некоторых свойств объекта Streamwriter, а именно типа кодировки (мы не указали кодировку, поэтому по умолчанию используется UTF-8) и провайдера формата: //Отображаем кодировки •• 1 > . Г® ,у».. ConsoielWriteuneVEricodina type i * .* sWrite>F,EncQdirigt;>Strlaig()); // Отображаем провайдер1 формата. (^s<de;WriteiJihef;Format Provider a ” + sWriter FormatProvlder,. ToString()); Выполним запись в файл; и здесь первым выберем метод WriteLine(). Как можно видеть, его использование напоминает метод Console.WriteLine() — он также легко позволяет записывать значения переменных и свойств.
Потоки в .NET 69 sWriter.WritetineCToday is 0. *’< OateTime Today.DayOfWeek) ; r - г sWriter.WriteLlneC'Today we will mostly be using Streamwriter."); S .Г . « S'"» (int. b=P;i<5;i+-r) - -> " - ъ sWriter.WriteLineCValue 0/its square is 1”, f, 1*1) t Теперь будем использовать метод Write() для записи массивов символов и час- тей символьных массивов. Поскольку WriteO не устанавливает переход на новую строку в конце вывода, следует использовать в начале строки управляющую после- довательность \г\п, чтобы гарантировать переход на новую строку перед записью текста: sWriteг.Write(“Arrays can be written : "J; / char£l myarray = new charQV, *r'^ ’r’., *a\ 'y’;: •' .• К ' /,; sWriter Wrjte(myarray); sWriter.WriteLine(“\r\nAnd parts of arrays can be written"); у sWriter.Write(myarray„ 0., -3); • . Ж. .av: > .»sWnter Closet); -k л:----л; • fS.Close(). ' zV } ft'! При просмотре в NotePad выходного файла видим, что в нем содержатся следу- ющие данные: Сериализация Сериализация — это процесс выбора объектов и преобразования информации об их состоянии в форму, которая может сохраняться или переноситься. Сохра- ненный или перенесенный объект затем десериализуем, чтобы воссоздать его исходное состояние. Таким образом, вы можете, сериализовав объект, передать по- лученную информацию по сети и восстановить объект и его исходное состояние из другого приложения или системы. Вот некоторые основные области, где сериализация дает преимущества: □ Доступность — компонент можно сохранить в файле и обращаться к нему в любое время. □ Время жизни — сохранение объекта с его состоянием продлевает ему жизнь. Обычно, когда вы закрываете приложение, все связанные с ним объекты ав- томатически уничтожаются.
70 Глава 2 □ Использование в. сетевых приложениях — сложная форма объекта была пре- образована в формат, подходящий для передачи через сеть и, возможно, че- рез брандмауэры. О Надежность — сохраненный объект можно воссоздать “как он есть”. В этом разделе мы рассмотрим два способа: □ Сериализация в формат XML □ Сериализация с помощью объектов форматирования в двоичный код Сериализация в формат XML Сериализация объекта в формат XML имеет определенные преимущества — в первую очередь вы трансформируете специфичную для системы информацию о состоянии в текст, который можно легко переслать по сети и через брандмауэры. Однако в полученных XML-данных не сохраняются типы разнообразных используе- мых полей, вместо этого свойства, поля и возвращаемые значения сериализуются в XML-формат. Эта особенность полезна, если нужно передавать значения, а не де- тальную информацию о самом объекте. Класс XmlSerializer из пространства имен System.Xml.Serialization обеспечи- вает функциональные возможности сериализации и десериализации объектов в XML-формате. Для сериализации класса есть два простых правила. □ Класс должен поддерживать используемый по умолчанию открытый конст- руктор без параметров. Это требование связано с тем, что при воссоздании объекта через процесс десериализации сначала экземпляр объекта создается конструктором по умолчанию, а затем из входящего потока данных устанав- ливаются открытые свойства. Если конструктор по умолчанию отсутствует, .NET Framework не будет знать, как создать объект. □ Сохраняются только открытые свойства, поддерживающие операции get и set, и открытые члены данных. Это объясняется тем, что процесс сериализа- ции не может обращаться к закрытым и доступным только на чтение элемен- там данных. Имеются способы сериализации и этих данных, но они приводят к изменениям в самом классе. Для сохранения всех открытых свойств и элементов данных объекта больше ни- чего делать не требуется. В следующем примере класс Customer, имеющий поля для сохранения данных о потребителе, сериализуется в формате XML. Обратите внимание, что в этом клас- се есть закрытое поле: using System; using System.10; using System, Text; using System.Xml,Serialization; public class XmlSerialExample public class Customer V- public int Customerip; public string CustomerName; public DateTime SignUpPate; private decimal currentcredit;
Потоки в .NET 71 public void SetCuгrentCredit(decimal c) SC--. { currentcredit = c; » } Cpublic decimal GetCurrentCreditO return'currentcredit; /’ Определив класс Customer, создадим новый экземпляр и установим значения не- которых полей. public static void Main() { • - • // Готовим объект к сериализации Customer cm = new Customer(); cm.CustomerlD = 12, L(, cm.CustomerName = "Ward Littell”; n cm.SignUpDate = DateTlme.Now; „ cm. SetCurrentCradit(7& 23МЫ Теперь начинаем процесс сериализации. Создаем объект St reamWriter для выво- да полученного XML в файл Customer, xml, затем — объект XmlSerializer, передав ему тип объекта, который хотим сериализовать. После этого остается только вызвать метод Serialize() объектаXmlSerializer и передать ему St reamWrite г и объект, чье со- стояние надо сохранить: Console.WriteLine(“Now Serializing.... // Создаем объект Streamwriter Streamwriter writer = new StreamWriter(“Customer.xml"); // Создаем сериализатор XmlSerializer serializer » new XmlSerializer(typeof(Customer)); // Сериалиэируем объект serializer.Serialize(writer, cm); writer.Close<); На этом процесс сериализации завершен. Для проверки выполним десериализа- цию из файла Custome г. xml и создадим новый объект с тем же состоянием, что и у те- кущего объекта Customer. Console.WriteLine (“Sow Deserializing.... // Открываем и создаем поток Stream streamOut - new FileStream(“Customer,xml", FileMode.Open, FileAccess.Read); Поток, предназначенный для чтения данных из файла Customer xml, создан — остается только вызвать метод Deserializer) объекта XmlSerializer. С целью иллюстрации создадим новый объект XmlSerializer с именем deserializer. Метод Dese rial ize () возвращает объект, его тип нужно привести к типу Custome г, и мы полу- чаем воссозданный объект Customer: XmlSerializer deserializer = new XmlSerializer (typeof (Customer) ); / // Выполняем десериализацию сохраненного потока
72 Глава 2 3 I Customer recm = (Customer}deserializer.Deserialize(streamOut), a k ? w эдй - s ’ / *ч ц’* ' '• streamQut.Closewv. Наконец отображаем состояние нового объекта: // Отображаем состояние объекта Console.WriteLine (“Customer ID = 0‘, recm CustomerlD); Console.WriteLine (“Customer Name = 0", recm.CustomerName) ; Console.WriteLine ("Sign up date = 0", recm.SignOpDate); Console.WriteLine ("Current Credit * OT^ recm.jQetCurrentCreditO); < Console.Read(); Вывод из программы выглядит следующим образом — заметьте, что значение Currentcredit равно 0, а не 76.23, каким мы его установили первоначально. Почему это так, поймем, когда посмотрим на вывод XML-файла: Сериализованный файл Customer, xml показан на следующем снимке экрана. За- метим, что точный тип каждого поля в файле не сохранен — XML содержит только значения полей и значение закрытого поля currentcredit сериализовано не было. 3k C:\Networkmg\Streams\Custonier.xml - Microsoft Internet Explorer Сериализация с помощью объектов форматирования В пространстве имен System. Runtime. Serialization. Formatters находятся инстру- менты для сериализации состояния объектов в двоичный формат и в формат SOAP — класс BinaryFormatter обеспечивает функциональные возможности для се- риализации объектов в двоичный формат, а класс SoapFormatter сериализует в фор- мат SOAP. Рассмотрим оба класса.
Потоки в .NET 73 Двоичный формат сериализует состояние объекта, а также релевантную инфор- мацию, делая возможным точное воспроизведение объекта и его типов. Он также делает компактный формат. Формат SOAP не дает такого сжатого представления, но позволяет передать состояние вашего объекта, например, Web-сервису. Объекты форматирования позволяют сериализовать закрытые поля, и ключом к их использованию является атрибут [Serializable], которым отмечается класс, чтобы указать возможность его сериализации. Сериализация объекта проделайте следующие шаги: 1. Пометьте класс атрибутом [Se rial izable]. 2. Подготовьте состояние объекта для сериализации. 3. Создайте новый объект форматирования BinaryFormatter для двоичного формата или SoapFormatter для SOAP. 4. Вызовите метод Serialize() объекта форматирования и передайте ему поток для вывода результатов и сериализуемый объект. Десериализацию выполнить довольно просто — задается формат для десериали- зации объекта с помощью соответствующего объекта форматирования, вызывается метод Deserialize() этого объекта и полученный в результате объект приводится к тому типу, который надо воссоздать. Прежде чем рассматривать примеры сериализации, отметим несколько важных моментов о сериализации вообще и об использовании атрибута [Serializable]. Чтобы класс можно было успешно сериализовать, каждое его поле должно быть сериализуемым. Так, если бы в нашем классе Customer, отмеченном как [Seria- lizable] , было поле типа Add ress: (Serializable] public class Customer - { ' > ft public Address HomeAddress; IT } < ' -Г > public class’ Address public string StreetName; то сам класс Address следовало бы отметить как [Serializable], в противном случае сериализация невозможна. Заметим также, что атрибут [Serializable] не наследуется, таким образом, про- изводные типы не становятся автоматически сериализуемыми. Чтобы гаранти- ровать сериализуемость производных типов, их также нужно отметить как [Serializable]. В некоторых ситуациях вы не захотите сериализовать определенные поля класса, например, если их можно легко вычислить по другим полям или если они содержат секретные данные. Все такие поля вы можете отметить атрибутом [NonSe- rialized], и тогда они не будут сериализованы. [Serializable] й ’ public class Customer ' ’ЛЗ | . -X 2 r >- • «. \ public string CustomerName;
74 Глава 2 [ NoriSe r ialized ] public string FirstName; л Г1 I В следующем примере демонстрируется объект двоичного форматирования. После него рассмотрим SOAP-форматирование. us^ng System; using System.10; using System.Text; using System. Runtime.Serialization; using System.Runtime.Serialization.Formatters Binary; public class BinSerialExample { [Seria Lxzable] public class Customer { public int CustomerlD; public string CustomerName; public DateTime SignUpDate; private decimal cun entCredit; jpa public void SotCurrentCredit(decimal c) currentcredit «к ) V v public decimal GetCurrentCreditO V” return currehtGreditrЛ J V*. } public static void Main() { // Готовим объект к сериализации Customer cm = new CustomerQ; cm.CustomerlD = 12; cm.CustomerName = “Ward Littell”; cm.SignUpDate=DateTime.Now; cm.SetCurrentCredit(76.23M); Console.WriteLine(“Now Serializing..,.’’); И Создаем объек- потока для сохранения Stream stm » new FileSI ream(“BinCustomer. bin”.. FileMode Create, - • FileAccess. Write); Наш поток построен, и теперь создадим объект BinaryFormatter и выполним се- риализацию: // Сеоиализуем объект, используя двоичный фоомат BinaryFormatter inFormatter = new BinaryFormatter(); , 1 r inFormatter.Se'lalizeistm, cm>; ’Ч1 * . j.. r.:.- T • stmXloseOf ... .. ;Ч' Console WrlteLineC'Now Deserializing.. // Открываем й создаем поток4 r: » e Stream streamOut = new ГйеЗтгеапгС'ВхпСизготег.ЬьП", FileMode Open, '' " FileAccess.Read)^, •
Потоки в .NET 75 Для десериализации создадим объект BinaryFormatter, вызовем метод Dese- rial ize() и затем приведем результирующий объект к нужному типу Customer: // Выполняем десериализацию сохраненного потока BinaryFormatter outformatter = new BinaryFormatterO; Customer recm * (Customer)outFormatter Deserialize(streamOut); streamOut.Closet); // Отображаем состояние объекта Console.WriteLine ("Customer ID = 0”, recm.CustomerlD); Console.WriteLine (“Customer Name = 0", recm.CustomerName); Console WriteLine (“Sign up date = 0”, recm.SignUpDate); Console.WriteLine (“Current Credit = 0", recm.GetCurrentCreditO); } ' . > } Wv Запустив на выполнение этот код, вы увидите, что поле cu г rentCredit было сери- ализовано. Порожденный файл назван BinCustomer.bin — если вы его откроете в Visual Studio .NET, то увидите следующее: SUrtP^ge | 00000000 * 0000010 00000020 00000030 00000040 00000050 00000060 00000070 00000080 Э0000090 000000*0 ооооооьо ООООООсО OOOOOOdO k>i 00 69 2Е 75 54 42 2В 73 72 0D 00 00 __ ГС АО ОС 7А 30 74 6F 69 43 74 4Е 63 00 00 02 65 2Е 72 6В 6Е 75 6F 61 75 08 О DF оо оо ffW 00 2С 30 61 65 53 73 6D 6D 72 0D 57 39 00 20 2С 6С 6Е 65 74 65 65 72 05 61 17 оо st 20 2С 3D 72 6F 72 0А 65 02 72 С4 43 65 43 20 6Е 69 6D 49 53 6Е 00 64 08 42 72 75 50 75 61 65 44 69 74 00 20 05 г' .7 ff di do do od“db do 69 6E S3 65 72 69 61 73 69 6F 6E 3D 30 2E 6C 74 75 72 65 3D 6E 75 62 6C 69 63 4B 65 6C 6C 05 01 00 00 00 6C 45 78 61 6D 70 6C 72 04 00 00 00 0A 43 0C 43 75 73 74 6F 6D 67 6E 55 70 44 61 74 43 72 65 64 69 74 00 00 ОС 00 00 00 06 03 4C 69 74 74 65 6C 6C 37 36 2E 32 33 0B .Ля % . >: os.. 6C .......CBinSerial 30 Im. V«raion"0.0 65 .0.0. Culture»na 79 utral. PublicKay 19 Tdkananull ...... 65 BinSarialExaaple 75 +Custoaox......Cu 65 atoaorlD Custoae 65 rMaaa SignOpData 01 .currantCradit.. 00 ................. 10 .. .Vaxd Littall. ...9....76 23. Все содержимое файла сохранено в двоичном формате. Обратите внимание на релевантную информацию, включающую информацию о версии, позволяющую из- бежать проблем совместимости. Чтобы использовать объект SOAP-форматирования, в приведенный выше код нужно внести минимальные изменения. Прежде всего нам нужно подключить про- странство имен SOAP-форматирования: using System.Runtime.Serialization.Formatters.Soap; Другие изменения в предыдущем коде связаны со сменой объекта форматирова- ния — мы используем класс SoapFormatter для сериализации в SOAP-формат и изме- няем имя файла для результата сериализации: /7 Создаем объект потока для сохранения Stream stm = new FileStream(“SoapCustomer.xml", FileMode.Create, FileAccess, Write» .// Сериализуем объект, используя SOAP-формат SoapFormatter inFormatter « new SoapFormatter^); inFormatter. Serialize (stm, cm) Stm. Closet); Console.WriteLineC“Now Deserializing Д”); г. X/ Открываем и создаем поток. ; 5
76 Глава 2 Stream streamOut » new ..FileSt ream("SoapCustomer.xml*\ FUel-lode.Open, fe. ’ * леЛ • — Ж ^JHeAccess.Readjf^^^^lfW&e;-..Ж.: '"'‘S' ' - Я <&> ГГ tgt I • ., Я Г 4fe$KSg5|U; 5 " Ж // Выполняв1' десериализацию на сохраненном потоке '? SoapFor natter outFormatrer » .ew SoapFormattefO, -..» * Ж Customer recm = (Customer )ourForma!:terDeserialize( st reamOut); > «Ж* - 2®- -S ’ e: *: | " streamOut.Close(J; '.да^ 1 skw Выполнение этого кода производит следующий файл SoapCustomer. xml. Это кор- ректное SOAP-сообщение, которое можно передать, например Web-сервису. Итоги В этой главе мы рассмотрели класс Stream из пространства имен System. 10 и не- которые порожденные от него классы, представляющие специфичные типы по- тока, характеризуемые их базовыми устройствами. Рассмотрели класс FileStream, позволяющий обращаться к файлу на диске, классы Memorystream, Networkstream и другие классы. Мы узнали, что методы, которые они наследуют от класса Stream, делают обработку потоков довольно простой. Платформа .NET предоставляет и другие разнообразные классы в пространстве имен System. 10 для обработки потоков и передачи данных более сложных, чем набо- ры байтов. Мы рассмотрели классы BinaryReader и Binarywriter, предназначенные для работы с двоичными файлами, классы StreamReader и Streamwriter для работы с текстовыми файлами. Наконец мы узнали, как сериализовать и десериализовать объекты через XML-формат и с помощью объектов форматирования в двоичный формат и SOAP-формат.
ГЛАВА 3 Сетевое программирование в .NET D U главе 1 мы познакомились с организацией сетей и рассмотрели структуру и применение разных сетевых протоколов, в том числе TCP, UDP, IP и DNS. В дан- ной главе приступим к сетевому программированию с использованием классов из пространства имен Sy stem. Net. В первую очередь вкратце обсудим все классы из пространства имен System. Net, а затем перейдем к более детальному их рассмотрению. Для тех классов, которые не освещены здесь, дана ссылка на соответствующую главу книги, где они рассмотрены более полно. Сетевые классы, обсуждаемые в данной главе, играют важнейшую роль в остальных главах книги. В частности, рассмотрим следующие темы: □ Обзор классов System.Net О Работа с URI □ IP-адреса □ Таблицы DNS □ Запросы и ответы □ Аутентификация и авторизация □ Разрешения Классы пространства System.Net—обзор Пространство имен System. Net содержит сетевые классы для поиска IP-адресов, сетевой аутентификации, разрешений, отправки и получения данных. Рассмотрим эти классы, рассортировав их по группам.
78 Глава 3 Поиск имен Чтобы получить IP-адрес из DNS-имени хоста или получить имя хоста из IP-адре- са, можно использовать класс Dns. Класс DnsPermission представляет разрешение, необходимое для поиска Имени. DnsPermissionAttribute — это класс атрибута, позво- ляющий отмечать сборки, классы и методы, нуждающиеся в этих полномочиях. 1Р-адреса IP-адреса обрабатываются в классе IPAddress. У одного хоста может быть не- сколько IP-адресов и алиасов. Вся эта информация содержится в классе IPHostEnt гу. Когда мы ищем имя, класс Dns возвращает объект типа IPHostEnt гу. Аутентификация и авторизация В классе AuthenticationManager есть статические методы, предназначенные для аутентификации клиентского пользователя. В этом классе-утилите используются модули, реализующие интерфейс lAuthenticationModule. Класс AuthenticationManager обращается к этим модулям, чтобы идентифицировать пользователя. Модули аутен- тификации получают информацию запроса и данные о личности пользователя с помощью интерфейса ICredentials и возвращают объект Authorization для автори- зованных Пользователей, которым разрешается использовать ресурс. Приложение-клиент может передать данные о пользователе на сервер экземпля- ром класса Netwo rkC redent ial. Эти данные о пользователе могут быть занесены в кэш вCredentialCache. В этой главе дается обзор механизма аутентификации и авторизации, а детально он представлен в главе 11. Запросы и ответы Абстрактные базовые классы, предназначенные для отправки запросов на сер- вер и получения ответов, называются WebRequest и WebResponse. В пространстве имен System. Net имеется несколько специальных реализаций этих классов для НТГР и доступа к файлам: HttpWebRequest, HttpWebResponse, FileWebRequest HfileWebResponse.
Сетевое программированиев .NET __ 79 Классы HttpXXX также используют еще один класс из пространства имен Sys- tem. Net: класс HttpVersion применяется для указания версии HTTP. У классов HttpWebRequest и HttpWebResponse есть свойство Protocolversion, определяющее вер- сию HTTP, — HttpVersion. VersionlO или HttpVersion. Version'll. Версия HTTP 1.0 ис- пользовалась на заре Интернета и продолжает использоваться некоторыми Web-серверами, текущая версия HTTP 1.1 включает некоторые дополнительные возможности, например, может поддерживать открытое соединение для несколь- ких запросов. В этой главе обсуждается функциональность классов WebRequest и WebResponse и спе- циальные реализации FileWebRequest и FileWebResponse. Протокол HTTP и связан- ные с ним классы представлены в главе 8. Разрешения, необходимые для классов запросов и ответов, определены в классе WebPermission и классе атрибута WebPermissionAttribute. Класс компонентов WebClient облегчает использование WebRequest и WebResponse из Visual Studio . NET. Этот класс порожден от класса Component и поэтому может использоваться с функцией буксировки мышью из панели Toolbox. Однако по умол- чанию он не сконфигурирован для использования Toolbox. С классом WebClient не- трудно копировать файлы с сервера и на сервер. Управление соединениями Классы ServicePoint и Service Point Manage г играют важную роль для НТТР-соеди- нений. Для ресурса экземпляр класса ServicePoint связан с URI и может обрабаты- вать несколько соединений. Класс-утилита Se rvicePointManage г управляет объектами ServicePoint, создавая новые или отыскивая существующие объекты. Пропускную способность приложения, которое одновременно запрашивает множество данных с сервера, можно повысить, увеличив число соединений на базе приложения. Чтобы ограничить выделение сетевых ресурсов, по умолчанию макси- мальное число соединений с одним и тем же сервером устанавливается равным двум. Далее в этой главе в разделе “ Опрос соединений” говорится о том, где указывает- ся это значение и как его можно изменить. Создание пулов соединений полезно для приложений среднего уровня, соединя- ющих ресурсы Интернета в интересах конкретного пользователя. Можно повторно использовать соединения из групп соединений, где каждая группа связана с данными о личности пользователя.
80 Глава 3 Записи cookies Cookies — это хранящиеся на стороне клиента наборы данных, которые исполь- зуются сервером для запоминания некоторой информации между запросами. Когда для запросов данных с сервера используется такой Web-браузер, как Internet Explo- rer, он сам управляет приемом и сохранением записей cookies и посылает их об- ратно Web-серверу. При создании специального приложения, запрашивающего данные с Web-сервера, посылающего записи cookies, можно их считывать в объект класса CookieCollection, возвращаемый свойством Cookies объекта HttpWebResponse. С помощью класса CookieContainer передаются записи cookie серверу. Сама запись cookie представлена в классе Cookie. Записи cookies посылаются в заголовке протокола HTTP, поэтому вернемся к ним в главе 8 при рассмотрении протокола H TTP. Прокси-сервер Прокси-сервер используется в сетевой среде, чтобы адресовать соединение с Интернетом через одну систему (или несколько систем в зависимости от размера сети). Прокси-сервер может кэшировать страницы, запрашиваемые пользователя- ми, поэтому, если одна и та же страница запрашивается несколько раз, не нужно по- сылать запрос к Web-серверу, поскольку Web-прокси может сам ответить на этот запрос. Класс WebProxy используется для определения прокси-сервера, к которому следу- ет обращаться при запросах в Интернет. С классом GlobalProxySelection можно определить по умолчанию прокси-сервер, который должен использоваться для всех запросов, если иное не указано в конкретном запросе. <утмлига> GlobalProxySelection Сокеты Вместо Web-классов используются классы сокетов, при этом мы, приобретая до- полнительные возможности и гибкость, сталкиваемся с определенной сложностью. Большинство классов, которые используются в программировании сокетов, можно найти в пространстве имен System. Net. Sockets. Программирование сокетов не только позволяет осуществлять связь, ориенти- рованную на соединения, как в случае с HTTP, но также и реализовывать связь без установления соединений, которая используется при групповой рассылке или ши- роковещательной передаче с UDP. Программирование сокетов — чрезвычайно гиб- кий механизм, позволяющий пользоваться самыми разными протоколами: GGP, ICMP, IGMP, IPX и SPX.
Сетевое программирование в .NET ; 81 В следующих главах рассмотрим программирование сокетов с протоколами TCP и UDP для передачи данных как через установленные соединения, так и без них, при широковещательной и групповой передачах. Обзор наиболее важных классов из пространства имен System.Net закончен, и теперь более детально рассмотрим некоторые из этих классов, составляющие основу для оставшейся части книги. Работа с URI Каждый день мы используем универсальные идентификаторы ресурса (Uniform Resource Identifiers, URI), когда что-то ищем в WWW. URI нужны, чтобы иденти- фицировать и запросить новый вид ресурса. Используя URI, нужно обращаться не только к Web-страницам, но и к FTP-серверу, Web-сервису и локальным файлам. Вместо URIчасто используется термин “унифицированный указатель ресурса” (Uniform Resource Locator, URL). URI - общий термин, используемый для ссылок на ресурсы. URL - это URI, связанный с такими популярными схемами URI, как http, ftp и mailto. В технической документации термин URL больше не употребляется. Еще один термин может быть вам уже известен - “унифицированное имя ресурса” (Uniform Resource Name, URN). URN - это стандартизированный URI, используе- мый для указания ресурса независимо от его расположения в сети. URIопределен в REC 2396 (http://www.ietf.org/rfc/rfc2396.txt), URN- в RFC 2142 (http://www.ietf. org/rfc/rfc2141. txt). URI может выглядеть так: http://www.wrox.com В этом URI для ссылки на Web-сайт используется схема http. mailto:ch ristian@nagel.net Схема mailto используется для адресов электронной почты. news:msnews.microsoft com Через схему news можно обращаться к группам новостей USENET. Проанализируем части URI, ссылающегося на страницу Web-сайта компании Global Knowledge: http://www.globalknowledge.net:80/training/generic.asp?pageid=1078&country=DACH □ Первая часть URI называется схемой (scheme). Схема определяет простра- нство имен URI и может сузить синтаксис следующего за схемой выражения. Многие схемы названы по соответствующим протоколам (как http, ftp), ко- торые они используют, но это не является обязательным. В нашем примере идентификатором схемы является http. Ограничитель схемы (:// в этом при- мере) отделяет схему от остальной части URL О После ограничителя схемы следует имя сервера или IP-адрес в десятичной за- писи с точками, например www. globalknowledge. at. □ За именем сервера или IP-адресом находится номер порта, определяющий со- единение с конкретным приложением на сервере. Если номер порта не задан, используется номер порта, устанавливаемый для этого протокола по умолча- нию (например, порт 80 для HTTP).
82 Глава 3 □ Путь определяет страницу (и каталог) запрошенного ресурса. Он необяза- тельно представляет физический файл на сервере, а может создаваться дина- мически. В данном случае путь имеет вид /training/generic. asp. О От пути символом ? отделена последняя часть этого URI, называемая запро- сом (query). В нашем примере запрос определен строкой pageid=1078&count- ry=DACH. Строка запроса может состоять из нескольких компонентов, каждый из которых задает переменную и значение, объединенные символом =. * Несколько компонентов запроса могут комбинироваться символом &. Так, в нашем примере первый компонент — pageid=l078 с переменной pageid и значением 1078, а второй компонент — count ry=DACH. □ Разделы внутри ресурса можно отождествить с фрагментами. Фрагменты ис- пользуются для ссылок на разделы внутри HTML-страницы. В разработке Web-страниц фрагменты также называются закладками (bookmarks). Символ # отделяет идентификатор фрагмента от пути. В URL http://www. micro-_ soft.com/net/basics/glossary.asp# NETFramework фрагментом является строка #.NETFramework. Если символ # добавлен в строку запроса, то это уже не фрагмент. В URL мо- жет присутствовать строка запроса или фрагмент, но не то и другое одновре- менно. В URI зарезервировано использование нескольких символов — они не могут вхо- дить в имена хостов или путь, поскольку представляют собой специальные симво- лы-разделители. В URI зарезервированы следующие символы: , / ? @ & = + $ , Класс Uri Класс Uri из пространства имен System инкапсулирует универсальный иденти- фикатор ресурсов. Он содержит свойства и методы для анализа, сравнения и комби- нирования URL Конструирование объектов Uri Можно создать объект Uri, передав конструктору строку URI: Uri uri = new Uri(“http://msdn.Microsoft com/code/default.asp’’);^5k^ Если уже есть базовый объект Uri, можно создать новый URI, комбинируя базо- вый URI с относительным URI: Uri PaseUri = new Uri(“http://msdn. microsoft.com”); s < Uri newUri = new Uri(baseUri, “code/default asp-); Если базовый URI уже содержит путь, он игнорируется. В качестве базы для нового URI берутся лишь схема, порт и имя сервера. Распространенные схемы В классе Uri есть несколько статических полей только для чтения, позволяющих получить некоторые широко распространенные схемы. □ Uri.UriSchemeFile Файловая схема используется для доступа к файлам локально или на совмест- но используемых сетевых ресурсах, для которых могут применяться имена, соответствующие соглашению об универсальном назначении имен (Universal Naming Convention, UNC).
Сетевое программирование B-NET 83 □ Uri.UriSchemeFtp Протокол FTP со схемой ftp используется для получения файлов с ftp-сервера и, напротив, помещения файлов на ftp-сервер. □ Uri.UriSchemeGopher Протокол gopher был предшественником HTTP. Он предоставлял возмож- ности иерархического просмотра текстовой информации о содержании, в чем превосходил FTP, но скоро был заменен протоколом HTTP. □ Uri.UriSchemeHttp, Uri.UriSchemeHttps Эти две схемы хорошо известны: http и https. Схема https используется для за- щищенного обмена. Протокол HTTP представлен в главе 8. □ Uri.UriSchemeMailto Схема mailto используется для отправки почтовых сообщений. □ Uri.UnSchemeNews, Uri.UriSchemeNntp Схемы news и nntp применяются в группах новостей, использующих прото- кол NNTP. Проверка правильности имени хоста и схемы В классе Uri есть статические методы для проверки правильности схемы и име- ни хоста: Uri. CheckSchemeName() возвращает true, если имя схемы задано правильно, а метод Uri. CheckHostNahieQ не только проверяет имя хоста, но и возвращает значе- ние перечисления UriHostNameType, указывающее тип хоста. Возможные значения UriHostNameType перечислены в таблице: Значение UriHostNameType Описание Basic Имя хоста установлено, но его тип не определен. Dns Этот тип будет возвращаться особенно часто. В ответ налередачу строки с расширением домена или без него возвращается это значение. IPv4 Если переданная отрока содержит десятичную нотацию с точками, например 204 .148 170 161, возвращается тип IPv4 IPv6 Тип IPv6 возвращается, если имя хоста задается строкой IPv6 В IPv6 хост идентифицируется 128 битами вместо 32 битов в IPv4. Строка адреса IPv6 выглядит, например, так: 1080:0:0:0:8:800: 2бОС: 417А Unknown Если имя хоста содержит недопустимые символы, возвращается значение Unknown Метод U ri ChepkHostName () используется, чтобы проверить, правильную ли стро- ку ввел пользователь для имени хоста, но этот метод не позволяет узнать, существу- ет ли хост с таким именем и доступен ли он. Действительно ли существует имя хоста, проверяется через трансляцию имени хоста в IP-адрес с помощью класса Dns, как бу- дет показано далее в этой главе. Свойства класса Uri В классе Uri имеется масса свойств с доступом только на чтение, позволяющих обращаться ко всем частям URL
84 Глава 3 В следующей таблице используем приведенный ниже URI как пример, демонст- рирующий применение свойств: http://www.globalknowledge.net:80/training/generic.asp?pageid=l078&country=DACH Свойство Описание AbsoluteUri Это свойство показывает полный URI. Если указанный номер порта для протокола равен номеру порта по умолчанию, конструктор U г i автоматически его удаляет. - Для нашего примера значение свойства AbsoluteUri выглядит так: - http://www.globalknowledge.net/training/generic.asp?pageid=1078&c ountry=DACH - Если конструктору класса Uri передается имя файла, свойство AbsoluteUri автоматически помещает перед именем файла схему f ile://. Scheme Схема — первая часть URI, и в данном случае это свойство возвращает значение http. Host Свойств^Ноб! показывает имя хоста из URI: www. global knowledge, net Authority Если номер порта равен номеру, используемому протоколом по умолчанию, сво- ' йство Authority показывает ту же строку, что и свойство Host. Если использу- ется другой номер порта, то свойство Authority также показывает номер порта. HostNameType Тип имени хоста зависит от используемого имени. В данном случае получается то же самое значение перечисления UriHostNameType, которое было рассмотрено выше, — UriHostNameType. Dns. Port С помощью свойства Port получается номер порта — 80. AbsolutePath Абсолютный путь начинается после номера порта в URI и заканчивается перед строкой запроса. В этом случае он имеет значение /training/generic. asp. LocalPath Локальный путь дает значение /training/generic. asp. Как можно видеть, для запроса HTTP между AbsolutePath и LocalPath нет никакого различия. Различие появляется, если URI ссылается на совместно используемый сетевой ресурс. ДляURI в виде file:\\server\share\directory\file.txt свойство LocalPath возвращает только имена directory и file, а свойство AbsolutePath включает имена server и share. Query Свойство Query показывает строку, следующую после пути: ?pageid=1078&country=DACH. PathAndQuery Свойство PathAndQuery дает комбинацию пути и строки запроса: /training/generic.asp?pageid=1078&country=DACH. Fragment Если после пути следует фрагмент, он возвращается в свойстве F ragment. За путем, могут следовать только строка запроса или фрагмент. Фрагмент иден- тифицируется символом # Segments Свойство Segments возвращает массив строк, сформированный из пути. В данном случае у нас есть три сегмента: /, training/ и generic, asp. Userinfo Установленное в URI имя пользователя можно прочитать из свойства Userinfo. Передача имен пользователей распространена в протоколе FTP, и если указан не анонимный пользователь, например ftp://myuser@ftp. myserver. com, то свойство Userinfo вернет myuser.
Сетевое программирование в .NET 85 Кроме перечисленных, существует еще несколько свойств, возвращающих буле- вы значения, если URI представляет файл, путь UNC, адрес обратной связи или если для данного протокола используется номер порта по умолчанию. Это свойства IsFile, IsUnc, IsLoopback и IsDefaultPort соответственно. Изменение URI с помощью класса UriBuilder После создания конструктором экземпляр класса Uri не может больше изме- няться. Свойства класса Uri доступны только на чтение. Для динамического измене- ния URI можно использовать класс UriBuilder. Его свойства аналогичны свойствам класса Uri, но их значения можно как считывать, так и записывать. Благодаря досту- пу только на Чтение класс Uri гораздо быстрее класса UriBuilder. С такой идеей вы, возможно, знакомы, если работали с классами String и StringBuilder. В следующем примере передачей конструктору схемы, имени хоста, имени пор- та и пути создается экземпляр класса UriBuilder. Затем изменим путь, устанавливая свойство Path. Свойство Uri класса UriBuilder возвращает доступный только на чте- ние экземпляр класса Uri. UriBuilder uri1 = new UriBuilderC'httpS “www.gotdotnet“i 80, - ** i- "team/codewise/default.aspx"); uri1..Path = "team/codewise/associat ion. aspx’’; Uri uri2 = uriLUri: Абсолютные и относительные URI URI бывают абсолютными и относительными. Абсолютный URI начинается со схемы, за которой следуют имя хоста и необязательный номер порта. В абсолютном URI может быть путь, но он игнорируется, если абсолютный UR1 используется вмес- те с относительным URI. Относительный URI определяется только путем, поэтому требуется база, т. е. абсолютный URI, чтобы определить точное расположение ре- сурса. Если уже есть один URI, для доступа к ресурсу того же самого хоста достаточ- но задать относительный URI. Относительные URI короче, и поэтому требуется вводить меньше символов. Когда мы считываем ссылку с HTML-страницы, представленную тегом анкера, она может включать абсолютный или относительный URL Если она включает отно- сительный URI, и нам нужно его использовать в запросе, мы должны по нему со- здать абсолютный URI, поскольку в классе Uri может храниться только абсолютный URL В следующем примере кода базовый URI создается строкой http://www.got- dot net. com и сохраняется как база в переменной baseU гi. Путь /te'am/l ib га гies присо- единяется конструктором в конец базы. Если бы базовый URI включал также путь, то он был бы отброшен. Третий URI в этом примере использует в качестве базы resourcel и добавляет к нему /userarea/default. aspx. В Uri resourcel путь уже вклю- чен, но поскольку он используется как базовый URI, то этот путь игнорируется. Uri baseUri = new Uri(“http;//www.gotdotnet.com'-); Uri resourcel = new UnfbaseUn, “team/libraries”), Uri resource? = new Uri(resourcel. “/userarea/default.aspx"); Console.WriteLine(resourcel.AbsoluteUri); • Console WriteLine(resource?.AbsoluteUri); На следующем снимке экрана показан вывод полученных в результате абсо- лютных URI, которые сохранены в объектах Uri. Как видно на снимке, символ ‘/’, пройущенный между базовым URI и путем, добавляется автоматически, а путь, вхо- дящий в базовой URI, игнорируется.
86 Глава 3 Класс U ri не только поддерживает создание абсолютных URI из относительных, но также содержит метод MakeRelative(), получающий относительный URI из абсо- лютного. Следующий пример демонстрирует создание относительного URI из двух абсо- лютных: вызов resourcel. MakeRelative (resou гсе2) возвращает строку, которая пока- зывает, как можно перейти от URI объекта resourcel к URI объекта resouгсе2. Uri resourcel - . \ new Uri("http://www.gotdotnet.com/userarea/default.aspx”); Uri resources = new Uri(“ht|p://www.gotdotnpt com/team/li branes/”); Console.WriteLine(resoureel MakeRelative(resource2)) ; Console.Writeline(resource2,MakeRelative(resourcel)); > . \ .. Uri resources = new Uri(“http://msdn.microsoft.com/vstudio/default.asp”) ; Console.WriteLine (resources.MakeRelative (resources ) ) > Далее показан результат выполнения этого кода. Если мы хотим перейти от аб- солютного URI http://www.gotdotnet.com/userarea/default.aspx к URI http://www gotdotnet.com/team/libraries, то результирующим относительным URI будет ../team/libraries. Для возвращения нужен относительный URI ./../userarea/ default aspx. Как и при работе с локальной файловой системой, означает ссылку на родительский каталог. На примере третьей строки видно, что если методу MakeRelativeO передается совершенно другой URI, то возвращается абсолютный URI, потому что для доступа к ресурсу необходим абсолютный URL IP-адреса В сети TCP/IP компьютер можно уникально идентифицировать IP-адресом. Адреса IPv4 заключены в 32 битах, и для упрощения чтения IP-адреса он обычно представляется в десятичной записи с точками, например, 204.148.170.161, которая позволяет легче различать сети и подсети. Класс IPAddress инкапсулирует IP-адрес, позволяя его использовать со многими другими классами из пространства имен System. Net, и преобразовывает сетевой по- рядок байтов в системе, применяемой в хосте, и наоборот. Для создания объекта IPAddress в десятичной нотации с точками используется статический метод Parse():
Сетевое программирование в .NET 87 . 5 IPAddress address = IPAddress. Parse(“204.148.170.16Г); В самом классе IPAddress IP-адрес хранится в целочисленном поле, к которому можно обратиться с помощью свойства Address. Метод ToSt ring() возвращает деся- тичную нотацию с точками. Предопределенные адреса В классе IPAddress есть несколько открытых, доступных только для чтения по- лей, которые возвращают предопределенные 1Р-адреса. □ IPAddress. None возвращает адрес, который означает, что ни один сетевой ин- терфейс не должен использоваться. Это поле используется классом Socket, чтобы указать серверу не ожидать активности клиента. □ IPAddress. Loopback возвращает предопределенный адрес обратной связи 127 0 0 1. Этот адрес используется не для соединения с сетью, а для локаль- ных операций на одной машине. □ IPAddress. Broadcast возвращает широковещательный IP-адрес. В широкове- щательных сообщениях можно посылать данные всем компьютерам в локаль- ной сети. □ У компьютера может быть несколько сетевых плат с несколькими IP-адреса- ми. Сокет использует IPAddress. Any, чтобы ожидать действия на любом из этих сетевых интерфейсов. Порядок байтов, используемый в хосте и сети Работа в сети связана с соединением компьютеров, которые могут использовать разные процессоры и разные операционные системы. В зависимости от используе- мого процессора расположение байтов внутри поля типа short, int или long может различаться. Чтобы различать порядок байтов, используют термины little endian и big endian. При little endian самый младший байт сохраняется в младшем адресе памяти, а в случае big endian все наоборот — в младшем адресе памяти сохраняется старший байт. Процессоры Motorola используют big endian, а Intel-совместимые процессоры ' используют little endian. Важно определить порядок байтов, когда различные системы соединены вмес- те. К счастью, существуют стандарт на представление байтов в сети — сетевой поря- док байтов, — совпадающий с порядком big endian. Пользуясь IP-адресами, сохраняемыми в полях integer, и номерами портов, со- храняемыми в полях short, можно выполнить преобразование версии little endian системы Intel в сетевой порядок байтов. Класс IPAddress предлагает несколько ста- тических методов для преобразования типов данных short, int и long из порядка бай- тов хоста (little endian в случае процессоров Intel) в сетевой порядок байтов и наоборот. Метод IPAddress. NetworkToHostOrder() преобразует многобайтовое целое зна- чение из сетевого порядка байтов в порядок байтов хоста, метод IPAd- dress. HostToNetworkOrder() преобразует многобайтовое целое значение из порядка байтов хоста в сетевой порядок байтов. Для IP-адресов и номеров портов, используемых с сокетами, необходим сетевой поря- док байтов. Класс Socket платформы . NET работает с корректным порядком бай- тов. Однако порядок байтов, который используется для данных, отправляемых по
Глава 3 сети, - это полностью наша забота. Если мы не планируем обмениваться с система- ми, имеющими другую архитектуру процессора, в проверке порядка байтов нет ника- кой необходимости, но если мы обмениваемся с другими системами, то должны с этим, как-то разобраться. Если рассмотреть текстовые файлы на Unicode, содержащие двухбайтовые символы, то станет понятно, как это можно сделать. Текстовый файл на Unicode содержит начальный маркер FFFE. По этому маркеру, если он имеет вид FEFF, легко определить, что файл поступает с неверным порядком байтов. Увидев, что маркер содержит FEFF, надо переставить все пары байтов. Редактор текстов Unicode умеет изменять порядок байтов, поэтому в данном случае нет необходимости преобразовывать дан- ные перед отправкой в сеть Класе Dns Для соединения с сервером нужен IP-адрес сервера. Поскольку IP-адреса запом- нить непросто и они могут изменяться, используют имена DNS. В главе 1 рассказы- валось о функциональных возможностях DNS и о том, как сервер DNS выполняет разрешение имен в IP-адреса. В приложениях платформы .NET для разрешений доменных имен в IP адреса можно использовать класс Dns. Это не единственное назначение класса Dns, он со- держит также и другие механизмы. Разрешение имени в IP-адрес Чтобы получить IP-адрес из имени хоста, воспользуемся статическим методом Dns. Resolve(). Для одного имени хоста может быть сконфигурировано несколько IP-адресов. Поэтому метод Resolve() возвращает не только IPAddress, но и объект IPHostEnt гу, который содержит массив адресов, альтернативные имена и само имя хоста. В следующем примере с помощью метода Dns.ResolveO получаем IP-адреса для имени хоста www.microsoft.com. Используя свойство AddressList, которое возвращает массив объектов IPAdd ress, выводим IP-адреса на консоль. Затем, обращаясь к свойст- ву Aliases, выполним цикл по всем зарегистрированным именам и наконец выводим на консоль настоящее имя хоста. string hostname ? ‘’www.microsoft.com”; IPHostEntгу entry * Dns.ftesolye(t)ostname); ’’ E •» ". <s Л 5Я-? rY ' Ч Й? " s t Console.WriteLineC’IP Addresses for-0: ”. hostname); foreach (IPAddress address in entry.AddressList) Console.WriteLine(address.ToString()); Console. WriteLine(‘'\nAlias names 1 foreach (string aliasName in entry Aliases) Console WriteLine(aliasName); '' ' \ 4 %? I - и ' •• * Console WriteLine(“\nAnd the real hostname:”); Console.WriteLine(entry HostName); > Вывод на консоль из этого примера показывает, что имя хоста www microsoft.com — это лишь альтернативное имя для www.microsoft.akadns com, имеющего шесть IP-адресов:
Сетевое программирование в .NET , . 89 В классе Dns есть и другие статические методы, возвращающие объекты IPHost- Entry. Они различаются главным образом способом передачи имени хоста: Статический метод Dns Описание Resolve() Принимает DNS-имя хоста или IP-адрес в десятичной нотации с точками для разрешения IP-адресов. GetHostByNameO Этот метод принимает только DNS-имя хоста, но не 1Р-адрес. GetHostByAddress() Этот метод возвращает объект IPHostEnt ry, передавая или IP-адрес в виде строки в десятичной нотации с точками, или как объект IPAddress. Чтобы получить имя хоста локального компьютера, используем метод Dns.GetHostName(). Как разрешается 1Р-адрес? Существует много способов разрешения IP-адресов. Почему имя разрешалось, хотя его никто не конфигурировал с сервером DNS? Рассмотрим способы, которы- ми можно разрешить IP-адреса. В ранних версиях сети TCP/IP имена хостов разрешались только через файл HOSTS. В файле HOSTS устанавливаются соответствия между IP-адресом, именем хоста и необязательными дополнительными альтернативными именами. Такой файл с именем hosts можно найти на PC с Windows 2000 в каталоге <windir>\ system32\d rive rs\etc. Позднее была введена система доменных имен с серверами DNS, которым извест- но о соответствии между именами хостов и IP-адресами и наоборот — получение имени хоста по IP-адресу называется обратным поиском. Вместо изменения всех файлов HOSTS при добавлении нового IP-адреса нужно только добавить новый IP-адрес на сервер DNS. Клиентским системам нужно только знать о сервере DNS, и тогда IP-адреса можно разрешать с помощью этого сервера. Сервер DNS можно организовать со свойствами TCP/IP сетевой конфигурации. Чтобы освободить администраторов сетей от утомительной работы, связанной с назначением вручную IP-адресов всем клиентам в сети, можно использовать сер- вер протокола динамической конфигурации хоста (Dynamic Host Configuration Protocol, DHCP). В DHCP используются динамические IP-адреса для PC-клиентов. С DHCP у PC-клиента больше нет фиксированного IP-адреса. Но в то же время DHCP поставил новые проблемы перед серверами DNS и стал причиной введения
90 Глава 3 в Windows 2000 динамической системы имен доменов (DDNS). Используя DDNS, IP-адрес и имя хоста PC-клиента могут устанавливаться в сервере DNS автоматичес- ки, как только получен адрес DHCP. Подведем итоги. IP-адреса разрешаются с помощью файлов HOSTS и серверов DNS, но существуют и другие способы. В локальной сети кроме имени DNS использу- ется имя хоста NetBIOS. Если поиск DNS заканчивается неудачей, то для получения IP-адреса используются некоторые механизмы присваивания имен NetBIOS. Посмотрим, как они работают. Имена хостов NetBIOS В сети Microsoft у PC есть не только DNS-имена хостов, но и имена NetBIOS. Фун- кциональные возможности NetBIOS также могут использоваться с TCP/IP; это на- зывается NBT — NetBIOS над TCP/IP. Обычно имя NetBIOS — это то же самое DNS-имя, но без расширения доменного имени. Для разрешения имени NetBIOS используется файл LMHOSTS, похожий на файл HOSTS—он и находится в том же каталоге вместе с файлом HOSTS. Файл LMHOSTS. SAM, ко- торый вы можете найти на своем PC, это просто файл-пример, показывающий, как может выглядеть файл LMHOSTS. Если имя файла не может быть разрешено с по- мощью файла LMHOSTS, то разрешение имени NetBIOS зависит от типа узла NetBIOS. Различаются четыре типа узла: □ В-узел (широковещательный) Для В-узлов имя хоста NetBIOS разрешается с помощью широковещательной передачи. Если используются IP-маршрутизаторы, не пересылающие дальше регистрацию имени и запросы имен, имя разрешить нельзя. В-узлы представ- ляют неудачный выбор для крупных сетей, поскольку широковещательные сообщения увеличивают нагрузку на сеть. □ P-узел (точка-точка) - При использовании P-узлов имя хоста регистрируется сервером службы имен Интернета для Windows (WINS). Сервер AVINS аналогичен серверу DNS, не считая того, что он привязывает IP-адресу не имена DNS, а имена NetBIOS . Клиент может запросить у сервера WINS IP-адрес для имени хоста. Этот метод сокращает сетевой трафик, но он не работает, если сервера WINS нельзя достичь. а М-узел (смешанный) Конфигурация М-узла представляет собой смесь В-узла и P-узла. Для первой попытки используется широковещательное сообщение. Если оно не прино- сит результата, поскольку искомый хост не находится в этой же сети, запрос разрешить имя хоста направляется серверу WINS. Эта конфигурация полезна при наличии многих небольших подсетей, обменивающихся через медлен- ные каналы. □ Н-узел (гибридный) В Н-узле тоже смешаны В-узел и Р-узел, но здесь механизм P-узла используется первым, до механизма В-узла. Широковещательное сообщение используется как последнее средство, только если остальные механизмы не дали резуль- татов. Вы можете узнать тип узла, сконфигурированный для вашей системы, восполь- зовавшись утилитой командной строки ipconfig /all. Цо умолчанию устанавливает- ся обычно наиболее производительный тип Н-узла.
Сетевое программирование в .NET 91 Если изложенное разрешение имен не кажется чересчур сложным, можно еще отре- дактировать несколько элементов в реестре, чтобы изменить порядок поиска имени DNS и поиска имени NetBIOS - однако этот прием рекомендуется применять осто- рожно и постараться не перепутать ключи реестра. Конфигурацию этих значений можно найти в HKLM\System\Cu г rentCont rolSet\Se rvices\Net В APa ramete rs. Значение 4 переменной DhcpNodeTyре задает Н-узел. Асинхронное разрешение IP-адреса Опрос сервера DNS может занять некоторое время, все методы, о которых гово- рилось ранее, выполняются синхронно. В классе Dns есть встроенная поддержка асинхронного поиска DNS: у методов Resolve () и Get Host By Name () существуют асин- хронные версии. Обсудим только асинхронные версии метода GetHostByNameO, но и метод Resolve() устроен так же. Асинхронные версии метода GetHostByNameO называются BeginGetHostByNameO и EndGetHostByName(). Метод BeginGetHostByName() начинает опрос имени, но не ждет успешного завершения обработки запроса или завершения по тайм-ауту—он возвра- щает управление немедленно. Кроме имени хоста (которое передается как методу GetHostByNameO), этот метод принимает делегата AsyncCallback, определяющего, ка- кой метод следует вызвать, если имя хоста будет разрешено или истечет интервал тайм-аута. Здесь используется метод на уровне класса DnsLookupCompletedO, имею- щий тот же тип возврата и ту же сигнатуру, как они определены делегатом AsyncCallback. using System; using System.Net; class AsyncDnsDemo ф private static string hostname = “www.wiox.com”;, static void Maih(stnng[] args) < if (args.Length != 0) hostname « a’-gsl.O]; * ' J Dns.BeginGetHostByName(hostname, new AsyncCaliback<DnsLopkupCompleted), null); . Console,WriteLine("Waiting for the results... '); *- Console.ReadLine(); ) Как только поиск DNS завершен, вызывается метод DnsLookupCompleted(), и полу- чается результат поиска имени методом Dns. EndGetHostByName(). Затем обратимся ко всем IP-адресам, альтернативным именам и реальному имени хоста, как в синхрон- ном примере: private static void D.i s Loo kupCom pl eted( I Async Result ar) IPHostEntry entry = Dns.EndGetHostByNaiue(ar); Console.WriteLineC*IP Addresses for 0: ”, hostname); foreach (IPAddress address in entry.AddressList) Console.WriteLine(address.ToStringO) ;
92 Глава 3 Console.WriteLine("\nAlias names:’’); foreach (string aliasName in entry Aliases) Console.WriteLine(aliasName); Console. WriteLine(’’\nAnd the real hostname:”); Console.WriteLine(entry.HostName); } В качестве альтернативы передаче представителя методу Beg i nGet Host ByName () можно использовать возвращаемую ссылку на интерфейс lAsyncResult, чтобы прове- рить завершение поиска DNS по свойству IsCompleted. Как только поиск завершен, вызываем тот же самый метод, который использовали перед этим для чтения IP-ад- ресов и имени хоста: DnsLookupCompleted(). static void Main(string[] args) { if (args.Length != 0) hostname = args[0]; lAsyncResulx ar = Dns.BeginGetHostByName(hostname. null, null); while (I ar. IsCompleted) Console. WriteLineO’Can do something else../”}; System. Threading .Thread. Sleep(l00); DnsLookupCompleted(ar); } Запросы и ответы После того как было выполнено разрешение имени хоста, клиент и сервер могут начать обмен информацией. Сервер создает сокет и ожидает (listen (слушает)) вхо- дящие запросы от клиентов. Клиент соединяется с сервером, и затем клиент с серве- ром могут отправлять и получать данные. В главе 4, когда будут обсуждаться сбкеты, вы узнаете обо всех действиях, выполняемых за кадром. Здесь используются классы WebRequest и WebResponse, в реализациях которых все вопросы с сокетами решены; от- метим, эти классы очень просты в использовании. В следующем небольшом примере кода с помощью метода WebRequest. С reate () со- здается объект WebRequest. Метод Create() принимает объект Uri или строку, содер- жащую URL Метод GetResponseO возвращает объект WebResponse и соединяется с сервером, чтобы получить какие-либо данные. Для построчного чтения данных, полученных от сервера, используем объект St reamReade г. Uri uri = new Uri(”http://www.wrox.com”);s WebRequest request = WebRequest.Create!uri.) ; WebResponse response =? request GetResponseO; St ream stream - response.GetResppnseStream(); StreamReader reader - new StreamReader(stream); string line; while ((line a reader.ReadlineO) !- null) { ... Console. Writel-ine(line) ;= > response.Close!); reader.Close ();
Сетевое программирование в .NET 93 Рассмотрев простое приложение с объектами WebRequest и WebResponse, обсудим эти классы более детально. WebRequest и WebResponse Для запросов к серверу и ответов от сервера базовыми являются классы WebRequest и WebResponse. Сначала рассмотрим класс WebRequest. Статические методы WebRequest Описание Сreate() и CreateDefaultO В классе WebRequest нет открытого конструктора. Вместо конструктора для создания экземпляров класса могут использоваться статические методы Createf) и CreateDefaultO. Эти методы в действительности создают не объект типа WebRequest, а новый объект класса, производного от WebRequest, такого как HttpWebRequest или FileWebRequest. RegisterPrefix() 1 Используя метод RegisterPref ix(), можно зарегистрировать класс для обработки специфического протокола. Объекты этого класса будут созда- ваться методом WebRequest Create(). Этот механизм, называемый “под- ключаемыми протоколами” (pluggable protocols), описан далее в этой главе. Методы экземпляра WebRequest Описание GetRequestStreamO Метод GetRequestSt ream() возвращает объект потока, который может использоваться для отправки некоторых данных на сервер. BeginGetRequestSt ream() и EndGetRequestStreamO Асинхронный доступ к потоку запроса выполняется методами BeginGetRequestStream()и EndGetRequestStreamO. GetResponse() Метод GetResponse() возвращает объект WebResponse, который может использоваться для чтения данных, полученных от сервера. BeginGetResponseO и EndGetResponseO Как и для потока запроса, имеются асинхронные методы для получения потока ответа. AbortO Если метод BeginXX() начал асинхронную обработку, ее можно остановить методом AbortO. Свойства WebRequest Описание RequestUri RequestUri — свойство только для чтения, возвращающее URI, связанный с WebRequest. Этот URI может быть установлен в статическом методе Create() данного класса. Method Свойство Method используется, чтобы получить или установить „метод для конкретного запроса. Объект HttpWebRequest поддерживает HTTP-методы GET, POST, HEAD ит. д. Headers ) В зависимости от используемого протокола серверу может передаваться и от сервера может получаться различная информация в заголовках. Информация заголовка протокола содержится в коллекции WebHeaderCollection, к которой можно обращаться через свойство Heade rs.
94 Глава 3 продолжение таблицы ContentType и ContentLength Тип данных, отправленных серверу, определяется в свойстве ContentType. Могут быть разные типы данных такой длины, чтобы данные могли разместиться в массиве байтов. Тип содержания обычно определяет MIME-тип данных: image/jpeg, image/gif, text/html или text/xml. Credentials Если серверу требуется аутентификация пользователя, удостоверения личности пользователя можно установить через свойство Credentials. PreAuthenticate Для протоколов, поддерживающих предварительную аутентификацию, в свойстве PreAuthenticate можно установить значение true. По умолчанию Web-браузер сначала пытается обратиться к странице Web-сайта без аутентификации. Если Web-сайту требуется аутентификация, сервер отвечает, что для неидентифицированных пользователей доступ отклонен. Следующий запрос, выполняемый клиентом, содержит информацию аутентификации. Этого дополнительного цикла обмена можно избежать, если установить в свойстве PreAuthenticate значение true. Proxy В свойстве Proxy можно установить Web-прокси, который используется для этого запроса. ConnectionGroupName и свойстве ConnectionGroupName можно определить пул соединений, который должен использоваться с этим объектом WebRequest. Timeout Свойство Timeout определяет время в миллисекундах, которое необходимо для ответа от сервера. По умолчанию установлено значение 100 000 млс. Если в течение этого времени сервер не отвечает, порождается исключение WebException. Класс WebResponse используется для чтения данных от сервера. Объект этого класса возвращается методом GetResponseO, как видно при рассмотрении класса WebRequest. Методы WebResponse Описание GetResponseSt ream() Метод GetResponseStream() возвращает объект потока, который используется для чтения ответа от сервера. Об объектах потока было рассказано в главе 2. Close() Если объект ответа больше не нужен, его следует закрыть методом CloseQ. Свойства WebResponse Описание ResponseUri С помощью свойства ResponseUri мы можем считать URI, связанный с объектом ответа. Он может совпадать с URI объекта WebRequest, но может и отличаться, если сервер переадресовал запрос к другому ресурсу. Headers Свойство Headers возвращает коллекцию WebHeaderCollection, которая включает специфичную для протокола информацию о заголовках, возвращаемых от сервера. ContentType и ContentLength —V Как и в WebRequest, в этом классе имеются свойства ContentType и ContentLength. Они определяют природу данных, возвращаемых от сервера.
Сетевое программирование в .NET 95 Теперь, после знакомства со свойствами и методами классов WebRequest и WebRes- ponse, рассмотрим некоторые характеристики этих классов. Подключаемые протоколы WebRequest — это абстрактный класс, поэтому метод WebRequest. С reate () не может создать объект типа WebRequest — вместо этого создается объект класса, производно- го от WebRequest. При передаче HTTP-запроса методу WebRequest. Create() создается объект HttpWebRequest. Класс HttpWebRequest вместе с подробной информацией о протоколе HTTP будет обсуждаться в главе 8. При передаче запроса со схемой файла создается объект FileWebRequest. Как показано далее, схемы http, https и file предопределены в конфигурацион- ном файле .NET, файле machine. config. Конфигурационный файл можно найти в ка- талоге <windows>\Microsoft.NET\Framework\<version>\CONEIG. <configuration> <system.net> <webRequestModules> Odd prefix="http" type="System.Net.HttpRequestCreator" /> odd prefix= “https” type= “System.Net.HttpRequestCreator” /> Odd prefix="file” type=”System.Net.FileWebRequestCreator" /> </webRequestModules> </system.net> </configuration> Набор протоколов, используемых классом WebRequest, можно расширить про- граммно или добавив элемент в конфигурационный файл. Для поддержки нового протокола, отличного от схем http, https и file, нужно со- здать новый класс, производный от WebRequest, например FtpWebRequest для протоко- ла FTP. Этот класс должен переопределить методы и свойства базового класса и в них реализовать специфичное для протокола ^поведение. Кроме того, требуется определить класс-инициатор (factory class), создающий объекты класса FtpWebRequest. Такой класс-инициатор, используемый классом WebRequest, должен реализовать интерфейс IWebRequestCreate. Назовем этот класс FtpWebRequestCreator. Экземпляр этого класса должен быть зарегистрирован для схемы ftp с помощью класса WebRequest: WebRequest RegisterPrefix(J‘ftpv, new FtpWebRequestCreatorO); Если теперь схема ftp используется с методом WebRequest.CreateO, создается и возвращается в программу новый экземпляр класса FtpWebRequest: г FtpWebRequest request = (FtpWebRequest JWebRequest Create(“ftp://’ftp. microsoft com")-; Теперь объект request можно использовать для копирования файлов с FTP-сер- вера и на FTP-сервер аналогично тому, как будут использоваться объекты запроса в следующем разделе. Здесь мы не собираемся заниматься реализацией класса FtpWebRequest, но вы можете это сделать самостоятельно после чтения глав 4 и 5. Для программирования FTP-клиента требуется использовать классы сокетов с соедине- нием TCP. FileWebRequest и FileWebResponse Чтение и запись локальных файлов или файлов, находящихся на совместно ис- пользуемых устройствах, не очень отличаются от чтения и записи файлов, распо- ложенных на Web-серверах. Чтобы считывать и записывать файлы, используем
96 Глава 3 классы FileWebRequest и FiJeWebResponse. Однако многие методы и свойства, опреде- ленные в базовых классах WebRequest и WebResponse, не используются в производных классах, и в документации MSDN они лишь перечисляются, как “зарезервирован- ные для использования в будущем”. Для демонстрации возможного использования классов FileWebRequest и File- WebResponse создается простое приложение Windows-формы, в котором имя откры- ваемого файла можно ввести в текстовом поле, после чего файл открывается и отображается в многострочном текстовом поле (большое пустое поле на следую- щем рисунке). Открытый файл можно затем сохранить под другим именем. Чтобы облегчить чтение кода, далее приводятся элементы управления, исполь- зуемые с этим приложением. Тип элемента управления Имя Текстовое поле textFileOpen Текстовое поле textFileSave Кнопка buttonOpenFile Кнопка buttonSaveFile Текстовое поле textData Чтение из файлов Обработчик щелчка по кнопке Open открывает файл и записывает содержание файла в многострочное текстовое поле. Передадим имя файла методу WebRequest.Create(). Ставить схему file:// перед именем файла необязательно. Класс WebRequest создает объект Uri и использует его свойство AbsolutePath. Как ука- зывалось ранее, класс Uri автоматически предпосылает имени файла корректную схему. Поэтому передача имени файла классу WebRequest создаст объект FileWeb- Request, и требуется лишь привести его тип. Метод GetResponse() возвращает объект FileWebResponse, который сразу же используется для создания методом GetRes- ponseStream() объекта Stream. Обычными методами класса StreamReader считывается
Сетевое программирование в .NET 97 поток. Данные всего файла считываем в строку и передаем ее в свойство Text мно- гострочного текстового поля textData. private void buttonOpenFile_ClicK(ooject sender, System.EventArgs e) • 4s string fileName = textFileOpen.Text; 4 FileWebRequest request = (FileWebRequest )WebRequest.Create( filename); Stream st ream = request.GetResponse(). GetResponseSt ream(); StreamReader deader = new StreamReader(stream), textData.Text = reader. ReadToEndC).; reader CloseO; } Запустив приложение, введем имя файла, после чего файл открывается и ото- бражается в многострочном текстовом поле: Необходимо внести простое изменение, чтобы можно было читать файлы как из файловой системы, так и с Web-сервера. Если ввести в программу НТТР-запрос, это приведете к исключению неверного приведения типа, поскольку мы приводим тип объекта, возвращенного методом WebRequest.CreateO, к классу FileWebRequest. Изменив программу так, чтобы она не выполняла приведение к типу FileWebRequest, можно использовать любую схему: string fileName = textFileOpen.lext; WebRequest request = WebRequest.Create(fileName), Stream stream = request. GetResoonseO. GetResponseSt ream(); ,---____________— - Если не Используются методы и свойства, специфичные для производ- ного класса, приводить тип объекта WebRequest, возвращенного методом WebRequest. CreateO, к конкретному классу необязательно! , В версии 1 платформы .NET в классе FileWebRequest нет методов и свойств, спе- цифичных для этого класса и не определенных в базовом классе WebRequest. Однако в классе HttpWebRequest есть несколько специфичных методов и свойств, о которых будет рассказано в главе 8.
98 Глава 3 Запись в файлы Для записи данных обратно в файл реализуем обработчик щелчка по кнопке Save. Как и раньше, создадим объект WebRequest, передавая имя файла. Теперь вместо StreamReader используем Streamwriter. Кроме этого есть еще одно существенное из- менение в коде. Чтобы сделать поток “записываемым”, следует установить в свойст- ве Method значение "PUT". По умолчанию это свойство имеет значение “GET", указывая, что поток можно только считывать. private void OnFileSave(object sender, System.EventArgs e) { -л: ' ... / t - string fileName = textFileSave.Text; WebRequest request - WebRequest.Create(fileName); request.Method = "PUT”; Stream stream = request.GetRequestStream(); f Streamwriter writer = new StreamWriter(stream); write r, Wr it e(textData.Text); writer Closef); )* A. s* '<3 , 1 ‘ Мы узнали, как можно использовать классы WebRequest и WebRespondte, но они пред- оставляют и другие возможности, которые тоже рассмотрим. Формирование пула соединений По умолчанию число одновременно открытых соединений с сервером опреде- лено в конфигурационном файле machine, config, как можно видеть на следующем примере. В конфигурации, установленной по умолчанию, максимально с одним и тем же хостом может быть одновременно два соединения. <configuration> * <system.net> <connectionManagement> ** . <add address*”*" maxconnection*"?" /> </connectionManagement> </system net> </configuration> Параметры настройки можно переопределить, не только добавляя в конфигура- ционный файл новые элементы, но также программно для конкретных запросов, используя классы ServicePoint и ServicePointManager. Возможно создание пулов нес- кольких соединений и использование их по имени со свойством Connecti- ons roupName. Это полезно, если устанавливать одновременно несколько соединений с одним сервером (см. главу 8). ч Использование Web-прокси В локальной сети можно использовать прокси-сервер, чтобы направить интер- нет-доступ к конкретным серверам. Прокси-сервер может сократить число передач и сетевых соединений из Интернета и повысить благодаря кэшированию ресурсов производительность локальных клиентов. Прокси-сервер выполняет активное и пассивное кэширование. □ При пассивном кэшировании Web-ресурсы сохраняются в кэше прокси-сер- вера, как только клиент запрашивает ресурс. Если второй клиент запрашива- ет тот же самый ресурс, получать его снова от Web-сервера в Интернете не нужно, поскольку Web-прокси может ответить непосредственно из кэша, со- зданного при первом запросе.
Сетевое программирование в .NET 99 О С помощью активного кэширования системный администратор может скон- фигурировать конкретные Web-серверы и каталоги, которые должны кэши- роваться автоматически в соответствии со специальным расписанием, например в ночные часы. Этим способом пропускная способность, необходи- мая для Интернета, днем может быть сокращена, чтобы увеличить произво- дительность для часто используемых страниц. Прокси-сервер, настроенный по умолчанию, устанавливается из Internet Options в Control Panel. Кроме того, к средствам конфигурирования можно также получить доступ из Internet Explorer (Tools I Internet Options I Connections I LAN Settings): В данном случае Web-прокси-сервер имеет IP-адрес 172.31.24.21 и слушает порт 80. Как отмечено в переключателе Bypass proxy server for local addresses, этот про- кси-сервер не должен использоваться для Web-серверов в интрасети. Через кнопку Advanced... можно сконфигурировать разные прокси-серверы для разных прото- колов (HTTP, HTTPS или FTP) и выбрать конкретные Web-сайты, к которым про- кси-сервер не должен обращаться. Класс WebProxy Класс WebProxy используется для определения прокси-сервера. Свойства этого класса аналогичны настройкам, которые были рассмотрены вместе с конфигуриро- ванием прокси-сервера: Свойства WebProxy Описание Address Свойство Add ress имеет тип U ri и определяет URI прокси-сервера, IP-адрес или имя и номер порта. BypassLi st В свойстве BypassList можно получать и устанавливать в массиве строк URI, которые не должны использовать прокси-сервер. BypassArrayList BypassArrayList — это свойство только для чтения, возвращающее объект типа ArrayList, представляющий URI, которые устанавливаются в свойстве BypassList.
100 Глава 3 продолжение таблицы BypassP roxyOnLocal Credentials BypassProxyOnLocal — это логическое свойство, указывающее, должны ли с прокси-сервером использоваться локальные адреса. Если прокси-сервер требует аутентификации пользователя, в свойстве Credentials можно передать удостоверение личности пользователя. Web-прокси по умолчанию Устанавливаемый по умолчанию прокси-сервер, используемый для всех соеди- нений, задается в классе GlobalProxySelection. По умолчанию используется Web-про- кси, установленный из панели Internet Options, как только что было показано. В свойстве Select устанавливается другой прокси для всех вызовов метода WebRequest.GetResponse(). В следующей программе обратимся к информации из установленного по умол- чанию Web-прокси и выведем ее на консоль. Свойство Select класса Global- ProxySelection возвращает интерфейс IWebProxy. Для доступа к свойствам класса WebProxy приведем этот интерфейс к классу WebProxy. Затем обратимся к URI прок- си-сервера, определенного свойством Address. Свойство BypassList возвращает мас- сив строк, который выводится внутри цикла to reach. Наконец используем свойство BypassProxyOnLocal, чтобы вывести на консоль сообщение об использовании (или не- использовании) прокси для локальных адресов. WebProxy proxy - (WebProxy)GlobalProxySelectxon.Select; Console WriteLine(“Address of the proxy server: 0", proxy. Address); foreacn (string bypassAdcressin proxy BypassList)/ { Console. WriteLj. ne( “Not using the proxy server tor this. + >. “address: O', bypassAddress); * • ? - - • c .*< л ''»• hi C Console.WriteLine (“Рог local addresses the proxy server is Q used”.,’?' > V • • proxy BypassProxyOnLocal “not” r ЙО&.Ш Вывод на консоль выглядит следующим образом: - - - . ....................................................... - - . . Изменение WebProxy для конкретных запросов Вместо того чтобы использовать установленный по умолчанию Web-прокси для всех запросов, можно выделить другой прокси для конкретных запросов. В сети ис- пользуем несколько прокси-серверов, чтобы распределить нагрузку или по требова- ниям безопцсности. Для выбора другого прокси нужно лишь установить свойство Proxy класса WebRequest. WebRequest reouest » WebRr'<juest.Greate.("bttp//www wrox com”); request.Proxy = new WebProxy (“172 31.24,i28”, 8080)’; Свойство Proxy класса WebRequest принимает объект, реализующий интерфейс IWobP/-Gxy^Конечно, класс WebProxy реализует интерфейс IWebProxy. Перегруженный
Сетевое программирование в .NET 101 конструктор класса WebProxy принимает URI для прокси-сервера и также все пара- метры для конфигурирования объекта WebProxy, уже известные, например, наряду с другими параметрами удостоверение личности пользователя и список URI, для ко- торых прокси-сервер следует обходить. Аутентификация Если Web-сервер требует аутентификации пользователя, можно создать удосто- верение личности пользователя и передать его Web-запросу. При этом полезны сле- дующие интерфейсы и классы: ICredentials, Networkcredential и CredentialCache. Для аутентификации пользователя создадим объект типа NetworkCredential. Этот класс обеспечивает информацию с целью удостоверения личности пользователя для базовой аутентификации, аутентификации на основе дайджестов, NTLM и Kerberos. Конструктору класса NetworkCredential можно передать имя пользователя, па- роль и дополнительно домен, разрешающий доступ пользователя. NetworkCredential credentials .» /£* "W-. new NetworkCredential“UserName" "Password"); Для авторизации пользователя эту информацию удостоверения личности мож- но установить в свойстве Credentials класса WebRequest: Л WeoReauect request = Л - * • ; . * ^WebRequest «Create (’“http^Z/requiresiogon^ccyn/myfile.aSipx”;)' ^... - request:Credentials - credentials; ' Если нужна разная информация удостоверений личности для разных URI, мож- но, использовать класс CredentialCache, как показано в следующем коде. С таким кэ- шем также определяется тип аутентификации для конкретного соединения. Здесь используется базовая аутентификация для Web-сайта www.unsecure.com и аутенти- фикация на основе дайджестов для Web-сайта www.moresecure.com, для которого че- рез сеть посылается не пароль, а хеш-код. : j С rede nxial Cache CredentialCache » new Credent ialCaWie()/ »• CredentialCache Add (new Uri(“http://www. unsecure.com’’), Basic”, , . л. - new Netwo rkC reden tial(“usei name”, "password”)); CredentialCache Add(new Uri("http://wv.w rro resecure, com”), “Digest”; new NeiwoH<Credential("username”, "password** '"domain").);. Чтобы использовать удостоверение личности, полученное в начале сеанса Windows от пользователя, обратимся к удостоверению личности по умолчанию с по- мощью метода CredentialCache. DefaultCredentials(). По соображениям безопасности это удостоверение личности используется только для аутентификации типа NTLM, Negotiate и Kerberos, и из него невозможно прочитать имя пользователя и домен. Разрешения Всякий раз когда используются сетевые классы, требуются разрешения. Для вопросов сетевого обмена рассмотрим три типа разрешения: □ DnsPermission □ WebPermission □ Socketpermission Разрешение DnsPermission требуется для поиска имени DNS с помощью класса Dns. WebPermission используется классами из пространства имен System. Net, которые отправляют данные в Интернет и получают данные с помощью URL Socket-
102 Глава 3 Permission используется для приема данных на локальном сокете или соединения с хостом через транспортный протокол. Приложения, установленные локально в системе, пользуются полным довери- ем, поэтому все разрешения доступны по умолчанию. Приложения платформы. .NET также могут запускаться с общего сетевого ресурса, а сборки могут загружаться из Интернета — в этих ситуациях многие разрешения недоступны по умолчанию. Следовательно, для этих приложений надо конфигурировать параметры настройки безопасности. Сначала обсудим программные аспекты безопасности, после чего рассмотрим, как можно конфигурировать разрешения. DnsPermission Когда используется класс Dns для поиска IP-адреса, требуется разрешение DnsPermission. Для него различаем только значения “признать” и “отвергнуть”. За- просы DNS или абсолютно не ограничиваются, или не разрешаются совсем. WebPermission WebPermission требуется для таких классов, как WebRequest и WebResponse, чтобы от- правлять данные в Интернет и получать их из Интернета. В этом случае различаются разрешения “согласиться” (Accept) и “соединиться” (Connect). Разрешение Accept нужно для URI, используемых внутри классов' и ме- тодов; клиентским приложениям, использующим URI для соединения с сервером, требуется полномочие Connect. У класса WebPermission также есть список URI, с ко- торыми можно соединиться, и список URI, с которыми можно согласиться. Socketpermission Разрешения SocketPe mission нужны для классов сокетов из пространства имен System. Net. Sockets, с которым мы познакомимся в главе 4. Это разрешение — скмое гибкое из трех классов сетевых разрешений. Для серверных приложений, ожидающих запросы на соединения от клиентов, в конструктор передается значение перечисления NetworkAccess. Accept; клиентские приложения, соединяющиеся с серверами, используют значение NetworkAccess. Connect. Можем ограничить соединение конкретными хостами и номерами портов и определить используемый транспортный протокол. Использование атрибутов разрешения Если требуемое разрешение недоступно, программа завершается с исключени- ем SecurityException, как только вызван привилегированный метод. До порождения исключения пользователь мог какое-то время работать с приложением, и может по- терять данные, если не обработать исключение аккуратно. Хороший способ, позво- ляющий этого избежать, состоит в том, чтобы отметить сборку необходимыми разрешениями. Если для получения данных из Интернета используется класс WebRequest, требу- ется разрешение WebPermission. Можно отметить классы и методы, требующие раз- решения, атрибутом WebPermission (реализованным в классе WebPermissionAttribute) следующим образом: ГWebPemission(SecurityAction Demand, * .. " ConneciPattern="http //www.wrox com )] class Permission!)emo r- -?• '
Сетевое программирование^ .NET 103 В данном случае исключение SecurityException возникает, как только создается экземпляр класса PermissionDemo. Если хотим, чтобы эта проверка выполнялась при старте программы, можно применить атрибут WebPermission ко всей сборке: [assembly: VJebPermission(SecurityAction.RequestMinimum, ConnectPattern«"http://www.wrox.com")] Если этот атрибут WebPermission применен к сборке, при старте программы среда выполнения проверяет, имеет ли программа необходимое разрешение. Если она не обладает требуемым разрешением, выполнение прекращается немедленно, прежде чем пользователь ввел (и потерял) информацию. Используя утилиту командной строки permview, являющуюся частью .NET Framework SDK, отобразим необходимые разрешения сборки: А Параметры атрибута разрешения Со всеми атрибутами разрешения конструктору передается значение перечисле- ния Secu rityAct ion. Здесь рассмотрим только самые важные значения перечисления. Значения перечисления SecurityAction Описание / Demand и Deny Значения перечисления SecurityAction. Demand и SecurityAction. Deny можно использовать с классами и методами. Значением Demand указывается, что классу и ли методу требуется разрешение, значение Deny определяет, что это разрешение не нужно. RequestMinimum, RequestOptional и RequestRefuse Значением перечисления RequestXXX можно пользоваться только в контексте сборки, его нельзя указывать с классами и методами. RequestMinimum определяет, что это разрешение обязательно для использования программы. Значением RequestOptional сообщается, что программа может выполнить некоторую полезную работу без этого разрешения. В этом случае надо постепенно обработать исключение SecurityException. Значение RequestRefuse определяет, что это разрешение не нужно. Это значение используется в тех случаях, когда возможно неправильное применение разрешения, например вызов сборок, для которых у нас нет исходных текстов и которым мы не доверяем полностью.
104 Глава 3 С атрибутом WebPermission можно в дополнение к SecurityAction установить следующие свойства: Свойства WebPermissionAttribute Описание Accept и AcceptPattern В свойстве Accept определяется URI ресурса для использования с классом, методом или сборкой, к которым применен этот атрибут. В свойстве AcceptPattern указывается регулярное выражение, чтобы разрешить или запретить доступ к URL Connect и ConnectPattern Два свойства ConnectXX аналогичны свойствам AcceptXX, но отличаются тем, что применяются для строки соединения с URL В классе SocketPermissionAttribute определены следующие дополнительные свойства: Свойства SocketPermissionAttribute Описание Access В этом свойстве определяется допустимый метод доступа к сети. Допустимы только два строковые Значения: Accept и Connect. Значение Accept используется для серверного приложения, слушающего и принимающего соединения клиентов, а значение Connect предназначено для клиента, соединяющегося с сервером. Host В свойстве Host с использованием синтаксиса DNS или IP-адреса устанавливается имя хоста, к которому относится разрешение. Port Это строковое свойство, задающее номер порта, для которого требуется разрешение. Свойство может использоваться для ограничения приложений-клиентов некоторыми конкретными серверами Свойство имеет тип string, поскольку разные про- токолы необязательно определяют номер порта как целое число. Transport С помощью свойства Тransport ограничиваются сетевые соединения конкретным транспортным протоколом. Возможные значениями, Connectionless, ConnectionOriented, Тер и Udp. Значение Connectionless позволяет использовать все протоколы, не требующие устанавливать соединения, например UDP; значение ConnectionOriented позволяет использовать протоколы, ориентированные на установление соединения, например TCP. Сборки со строгими именами Запуская сетевое приложение из интрасети или Интернета, назначаем обсуж- давшиеся разрешения. Однако работу, которую требуется выполнить для назначе- ния этих разрешений всем приложениям интрасети или Интернета, не назовешь приятной. Она чрезвычайно трудоемка и сильно все усложняет. Гораздо лучше определить конкретную сборку или группу сборок и конфигурировать разрешения только для нее. < В платформе .NET для уникальной идентификации сборок могут использоваться строгие имена, предоставляющие также способ, не позволяющий портить сборки.
Сетевое программирование b .NET ~ 105 В этой книге не освещены все возможности сборок со строгими именами. Подробнее 1 о них можно прочитать в книге Professional C# 2nd Edition, Wrox Press (ISBN 1-86100-704-3). Чтобы создать строгое имя, нужно утилитой sn создать пару ключей — открытый и закрытый: >sn- kmykey.snk • Используя атрибут сборки AssemblyKeyFile (класс AssemblyKeyFileAttribute нахо- дится в пространстве имен System. Reflection), добавляем в сборку открытый ключ и сигнатуру. [assembly: AssemblyKeyFile(“../../mykey snk”)] Как увидим далее, сборка со строгим именем может использоваться для конфи- гурирования системы безопасности. Конфигурирование разрешений Приложения, установленные локально, по умолчанию пользуются полным дове- рием, и конфигурирование этих приложений необязательно. Если приложение ско- пировано из Интернета, оно не имеет разрешений по умолчанию—надо установить разрешения явно. При запуске приложений из интрасети по умолчанию есть раз- решения Dns, но необходимо явно сконфигурировать разрешения WebPermission и Socketpermission. Для конфигурирования разрешений имеется утилита командной строки caspol. ехе и Windows-приложение .NET Framework Configuration Tool в Control Panel. В первую очередь создается новый набор разрешений, который будет использо- ваться сетевым приложением. Если вы довольны существующим набором разре- шений, уже включающим требуемые разрешения, необязательно создавать новый набор. В приложении .NET Framework Configuration Tool создается новый набор разрешений, названный Network Permissions:
106 Глава 3 Щелчок по кнопке Next приводит в диалоговое окно для назначения разреше- ний самому набору разрешений. Этот набор включает необходимые разрешения: DNS для поиска имен и Web Access для классов WebXX. Можно конфигурировать их как неограниченные разрешения или ограничить Web Access конкретным URL Разрешение User Interface необходимо для Windows-приложений. Create Permission Set Assign Individual Pernrissions Io Permission Set Each permission set is a colection of many different permissions to various resources on the computer. Select the permission* that you would Bee to have in this Demission set. Eventlog Environment Variables FfelO Add» J «Remove] DNS Web Access User interface 'Я' File Dialog Isolated Storage Fie Message Queue OlEDB Performance Counter Printing Registry Reflection Security Service Contrder Socket Access SQLOent 3» ..'♦*3 Im» g Av. ‘ b*. Ал js c _ Для сборки, которой необходимы эти разрешения, создается новая группа кода Professional .NET Networking Zone:
Сетевое программирование в .NET 107 После создания группы кода можно задать тип условия, определяющего сборки, принадлежащие к этой группе. Условием может быть, например, каталог приложе- ния, URI или сайт. В данном случае выбирается условие “строгое имя” и импортиру- ется созданное ранее строгое имя сборки. Выбор в окне переключателей для имени и версии ограничит группу кода данной конкретной сборкой. Если не выбирать эти опции, группа кода будет включать все сборки, использующие тот же самый открытый ключ: Create Code Group Choose « condition type The menbetship condition determines whether or not an assembly meets specific requirements io get the permissions associated with a code group. - К &2S I^tro ,j Маял The Strong Name membership condMon Is true for al assertdes with a strong name that matches the one defined below. Assembles that meet this membership condition wl be granted the permissions associated with this code group. ',-W V?. S Й Provide the strong name's pubic key. The name and version are t !:'i JHub ' । '«s’.-4’ i* , • • ’.yfe • i ' । B^-ckey: |c024C3CC046C3X094C WO xZ J02W0 Ю02400005253413 X'-tej.-rl- ЬЙМ* ,-д г w. g ~® -vV S?" -* 'f- 1 • - . *ъзг< ’’**»* ,r“ • Import... | : Ue the Import button to recneve the strong name from an assembly. <Back [ Next> | Caned | Нажатие на кнопку Next выбирает созданный набор разрешений для этой груп- пы кода. С такой конфигурацией теперь можно запустить сетевое приложение с общего сетевого устройства или приложение, скопированное с Web-сервера. О разрешениях в .NET можно прочитать подробнее в книге Professional C# 2nd Edition, Wrox Press (ISBN 1-86100704-3). Итоги В этой главе мы представили обзор сетевого программирования с использова- нием классов .NET, принадлежащих пространству имен System. Net. После обзора классов System. Net мы обсудили класс Uri, предназначенный для работы с абсолютными и относительными URI, и разбили URI на составляющие его части. Затем мы рассмотрели возможности класса IPAddress, который не только охва- тывает работу с IP-адресами, но и имеет поддержку для преобразования порядка байтов в сети. »
108 Глава 3 Далее мы использовали класс Dns для разрешения имен хостов в IP-адреса. Пос- кольку поиск имен может потребовать значительного времени, мы узнали, как это сделать асинхронно. Мы познакомились с основными темами, связанными с запросами и ответами — более подробно они будут освещены в главе 8. Мы обсудили базовые классы WebRequest и WebResponse и применили их для чтения и записи простых файлов. Наконец мы обсудили два аспекта безопасности: аутентификацию для соедине- ний с Web-сайтами, требующими от пользователя регистрации, и разрешений, не- обходимых для сетевого приложения. Таким образом, мы заложили фундамент и можем продолжить рассмотрение программирования сокетов, а затем перейти к программированию для Интернета.
ГЛАВА 4 Работа с сокетами D U предыдущих главах рассказывалось о поддержке сетевого программирова- ния в .NET. В них было показано, как обрабатывать потоки в приложениях .NET, и были рассмотрены классы, предназначенные для работы с IP-адресами и поиском имен DNS. В данной главе начнем программировать сокеты. В частности, обсудим следующие темы: □ Что такое сокеты и типы сокетов □ Поддержка сокетов в.NET —класс System. Net Sockets. Socket □ Создание серверного приложения на сокетах TCP □ Установка опций сокетов О Создание асинхронного приложения на сокетах TCP О Разрешения сокетов Сокеты Сокет—это один конец двустороннего канала связи между двумя программами, работающими в сети. Соединяя вместе два сокета, можно передавать данные между разными процессами (локальными или удаленными). Реализация сокетов обеспечи- вает инкапсуляцию протоколов сетевого и транспортного уровней. Первоначально сокеты были разработаны для UNIX в Калифорнийском уни- верситете в Беркли. В UNIX обеспечивающий связь метод ввода-вывода следует алгоритму open/read/write/close. Прежде чем ресурс использовать, его нужно от- крыть, задав соответствующие разрешения и другие параметры. Как только ресурс открыт, из него можно считывать или в него записывать данные. После использова- ния ресурса пользователь должен вызывать метод Close (), чтобы подать сигнал опе- рационной системе о завершении его работы с этим ресурсом. Когда в операционную систему UNIX были добавлены средства межпроцессно- го взаимодействия (Inter-Process Communication, IPC) и сетевого обмена, был за- имствован привычный шаблон ввода-вывода. Все ресурсы, открытые для связи, в UNIX и Windows идентифицируются дескрипторами. Эти дескрипторы, или опи-
110 Глава 4 сатели (handles), могут указывать на файл, память или какой-либо другой канал свя- зи, а фактически указывают на внутреннюю структуру данных, используемую операционной системой. Сокет, будучи таким же ресурсом, тоже представляется дескриптором. Следовательно, для сокетов жизнь дескриптора можно разделить на три фазы: открыть (создать) сокет, получить из сокета или отправить сокету и в кон- це концов закрыть сокет. Интерфейс IPC для взаимодействия между разными процессами построен по- верх методов ввода-вывода. Они облегчают для сокетов отправку и получение дан- ных. Каждый целевой объект задается адресом сокета, следовательно, этот адрес можно указать в клиенте, чтобы установить соединение с целью. Типы сокетов Существуют два основных типа сокетов — потоковые сокеты и дейтаграм- мные. Потоковые сокеты Потоковый сокет — это сокет с установленным соединением, состоящий из по- тока байтов, который может быть двунаправленным, т. е. через эту конечную точку приложение может и передавать, и получать данные. Потоковый сокет гарантирует исправление ошибок, обрабатывает доставку и сохраняет последовательность дан- ных. На него можно положиться в доставке упорядоченных, недублированных дан- ных. Потоковый сокет также подходит для передачи больших объемов данных, поскольку накладные расходы, связанные с установлением отдельного соединения для каждого отправляемого сообщения, может оказаться неприемлемым для не- больших объемов данных. Потоковые сокеты достигают этого уровня качества за счет использования Протокола Transmission Control Protocol (TCP). TCP обеспечи- вает поступление данных на другую сторону в нужной последовательности и без ошибок. Для этого типа сокетов путь формируется до начала передачи сообщений. Тем самым гарантируется, что обе участвующие во взаимодействии стороны принима- ют и отвечают. Если приложение отправляет получателю, два сообщения, то гаран- тируется, что эти сообщения будут получены в той же последовательности. Однако отдельные сообщения могут дробиться на пакеты, и способа определить границы записей не существует. При использовании TCP этот протокол берет на себя разби- ение передаваемых данных на пакеты соответствующего размера, отправку их в сеть и сборку их на другой стороне. Приложение знает только, что оно отправляет на уровень TCP определенное число байтов и другая сторона получает эти байты. В свою очередь TCP эффективно разбивает эти данные на пакеты подходящего размера, получает эти пакеты на другой стороне, выделяет из них данные и объеди- няет их вместе. Потоки базируются на явных соединениях: сокет А запрашивает соединение с сокетом В, а сокет В либо соглашается с запросом на установление соединения, либо отвергает его. * Если данные должны гарантированно доставляться другой стороне или размер их велик, потоковые сокеты предпочтительнее дейтаграммных. Следовательно, если надежность связи между двумя приложениями имеет первостепенное значе- ние, выбирайте потоковые сокеты. Сервер электронной почты представляет при- мер приложения, которое должно доставлять содержание в правильном порядке, без дублирования и пропусков. Потоковый сокет рассчитывает, что TCP обеспечит доставку сообщений по их назначениям. TCP рассмотривается подробнее в следую- щей главе.
Работа с сокетами 111 \ Дейтаграммные сокеты Дейтаграммные сокеты иногда называют сокетами без организации соедине- ний, т. е. никакого явного соединения между ними не устанавливается—сообщение отправляется указанному сокету и, соответственно, может получаться от указанно- го сокета. Потоковые сокеты по сравнению с дейтаграммными действительно дают более надежный метод, но для некоторых приложений накладные расходы, связан- ные с установкой явного соединения, неприемлемы (например, сервер времени суток, обеспечивающий синхронизацию времени для своих клиентов). В конце кон- цов на установление надежного соединения с сервером требуется время, которое просто вносит задержки в обслуживание, и задача серверного приложения не вы- полняется. Для сокращения накладных расходов нужно использовать дейтаграм- мные сокеты. Использование дейтаграммных сокетов требует, чтобы передачей данных от клиента к серверу занимался User Datagram Protocol (UDP). В этом протоколе на размер сообщений налагаются некоторые ограничения, и в отличие от потоковых сокетов, умеющих надежно отправлять сообщения серверу-адресату, дейтаграм- мные сокеты надежности не обеспечивают. Если данные затерялись где-то в сети, сервер не сообщит об ошибках. (Об UDP см. главу 6). Кроме двух рассмотренных типов существует также обобщенная форма сокетов, которую называют необрабатываемыми, сырыми, сокетами (raw sockets). Сырые сокеты Главная цель использования сырых сокетов состоит в обходе механизма, с по- мощью которого компьютер обрабатывает TCP/IP. Это Достигается обеспечением специальной реализации стека TCP/IP, замещающей механизм, предоставленный стеком TCP/IP в ядре—пакет непосредственно передается приложению и, следова- тельно, обрабатывается гораздо эффективнее, чем при проходе через главный стек протоколов клиента. По определению сырой сокет—это сокет, который принимает пакеты, обходит уровни TCP и UDP в стеке TCP/IP и отправляет их непосредственно приложению. При использовании таких сокетов пакет не проходит через фильтр TCP/IP, т. е. никак не обрабатывается, и предстает в своей сырой форме. В таком случае обязан- ность правильно обработать все данные и выполнить такие действия, как удаление заголовков и разбор полей, ложится на получающее приложение — все равно, что включить в приложение небольшой стек TCP/IP. Однако нечасто может потребо- ваться программа, работающая с сырыми сокетами. Если вы не пишете системное программное обеспечение или программу, аналогичную анализатору пакетов, вни- кать в такие детали не придется. Сырые сокеты главным образом используются при разработке специализированных низкоуровневых протокольных приложений. На- пример, такие разнообразные утилиты TCP/IP, как trace route, ping или а гр, ис- пользуют сырые сокеты. Работа с сырыми сокетами требует солидного знания базовых протоколов TCP/UDP/IP и лежит за рамками этой книги. Порты Порт определен, чтобы разрешить задачу одновременного взаимодействия с не- сколькими приложениями. По существу с его помощью расширяется понятие IP-ад- реса. Компьютер, на котором в одно время выполняется несколько приложений, получая пакет из сети, может идентифицировать целевой процесс, пользуясь уни- кальным номером порта, определенным при установлении соединения.
112 Глава 4 Сокет состоит из IP-адреса машины и номера порта, используемого приложени- ем TCP. Поскольку IP-адрес уникален в Интернете, а номера портов уникальны на отдельной машине, номера сокетов также уникальны во всем Интернете. Эта харак- теристика позволяет процессу общаться через сеть с другим процессом исключи- тельно на основании номера сокета. За определенными службами номера портов зарезервированы — это широко из- вестные номера портов, например порт 21, использующийся в FTP. Ваше приложе- ние может пользоваться любым номером порта, который не был зарезервирован и пока не занят. Агентство Internet Assigned Numbers Authority (IANA) ведет пере- чень широко известных номеров портов. Обычно приложение клиент-сервер, использующее сокеты, состоит из двух раз- ных приложений: клиента, инициирующего соединение с целью (сервером), и сер- вера, ожидающего соединения от клиента. Например, на стороне клиента приложение должно знать адрес цели и номер порта. Отправляя запрос на соединение, клиент пытается установить соединение с сервером: Если события развиваются удачно, при условии что сервер запущен прежде, чем клиент попытался с ним соединиться, сервер соглашается на соединение. Дав согласие, серверное приложение создает новый сокет для взаимодействия именно с установившим соединение клиентом. Теперь клиент и сервер могут взаимодействовать между собой, считывая сооб- щения каждый из своего сокета и, соответственно, записывая сообщения. Работа с сокетами в .NET Поддержку сокетов в .NET обеспечивают классы в пространстве имен Sys- tem . Net. Sockets — начнем с их краткого описания. Класс Описание MulticastOption Класс MulticastOption (см. главу 7) устанавливает значение IP-адреса для присоединения к IP-группе или для выхода из нее. Networkstream Класс NetworkSt ream (см. главу 2) реализует базовый класс потока, из которого данные отправляются и в котором они получаются. Это абстракция высокого уровня, представляющая соединение с каналом связи TCP/IP.
Работа с сокетдми 114 продолжение таблицы. TcpClient Класс TcpClient (см. главу 5) строится на классе Socket, чтобы обеспечить TCP-обслуживание на более высоком уровне. TcpClient предоставляет несколько методов для отправки и получения данных через сеть. TcpListener Этот класс также построен на низкоуровневом классе Socket. Его основное назначение — серверные приложения. Он ожидает входящие запросы на соединения от клиентов и уведомляет приложение о любых соединениях. Рассмотрим этот класс в главе 5, когда будем обсуждать TCP. UdpClient I UDP — это протокол, не организующий соединение, следовательно, для реализации UDP-обслуживания в .NET требуется другая функциональность. Класс UdpClient (см. главу 6) предназначен для реализации UDP-обслуживания. SocketException Это исключение порождается, когда в сокете возникает ошибка. Рассмотрим этот класс далее. Socket Последний класс в пространстве имен System. Net. Sockets — ' это саг класс Socket. Он обеспечивает базовую функциональность . приложения сокета. Класс System.Net.Sockets.Socket Класс Socket играет важную роль в сетевом программировании, обеспечивая функционирование как клиента, так и сервера. Главным образом, вызовы методов этого класса выполняют необходимые проверки, связанные с безопасностью, в том числе проверяют разрешения системы безопасности, после чего они переправля- ются к аналогам этих методов в Windows Sockets API. Прежде чем обращаться к примеру использования класса Socket, рассмотрим не- которые важные свойства класса System. Net. Sockets. Socket: Свойство Описание AddressFamily Дает семейство адресов сокета—значение из перечисления SocketAddressFamily. Available Возвращает объем доступных для чтения данных. Blocking Дает или устанавливает значение, показывающее, находится ли сокет в блокирующем режиме. Connected Возвращает значение, информирующее, соединен ли сокет с удаленным хостом. LocalEndPoint Дает локальную конечную точку. ProtocolType Дает тип протокола сокета. RemoteEndPoint Дает удаленную конечную точку сокета. SocketType Дает тип сокета.
114 Глава 4 и некоторые методы System. Net - Sockets. Socket: Метод Описание Accept() Создает «оный сокет для обработки входящего запроса на соединение. 6ind() Связывает сокет с локальной конечной точкой для ожидания входящих запросов на соединение CloseO Заставляет сокет закрыться Connect() Устанавливает соединение с удаленным хостом. GetSocketOption() Возвращает значение Socketoption. IOControl() Устамаклпает для сокета низкоуровневые режимы работы. Этот метод обеспечивает низкоуровневый доступ к лежащему ооиоее экземпляру класса Socket Listen() Помещает сокет в режим прослушивания (птидиг-).Тготметод предназначен только для-серверных приложений. BeceiveO Получает данные от соединенного сокета. Toll() ОПРВДВЛЯВ! L i d I * L 1.. Li кЕТЗ* SelectO Проверяет статус одного или нескольких сокетов. Send() Отправляет данные соединенному сокету. Se tSocketOption() Устанавливает опцию сокета. diuitaovn() Создание приложения на потоковом сокете TCP В следующем примере используем TCP, чтобы обеспечить упорядоченные, надежные двусторонние потоки байтов. Построим завершенное приложение, включающее клиент и сервер. Сначала демонстрируем, как сконструировать на по- токовых сокетах TCP сервер, а затем клиентское приложение для тестирования на- шего сервера. Следующая программа создает сервер, получающий запросы на соединение от клиентов. Сервер построен синхронно, следовательно, выполнение потока блокируется, пока сервер не даст согласия на соединение с клиентом. Это приложение демонстрирует простой сервер, отвечающий клиенту. Клиент завер- шает соединение, отправляя серверу сообщение <TheEnd>.
Работа с сокетами 115 Вот полный код программы Socketserver, cs: using System: -Ц using Sy§tenrttJSteket3;x Using System.Net; - ' -'WW; using System;Text;^ jj/ ' public class SocketServer \ public static void\< Main(string- [] 1 args) ’Wfe ' '/-• .. ^4- ". y*. -V' v" . // устанавливаем для сокета локальную конечную точку >; Г” IPHostEntry iphuSt = Dns.ResolveC’localhost”), IPAddress ipA0dr = ipHost AddressList[Op, IPEndPoint ipEndPoint new IPEndPoint(ipAddr, 11000); й v * v;'"' создаем co^r lcp/Ip w»PlW dti&Lendr liew 8рске€(А00ге$зЕатх1у.1пЖНё1*ОГкл • ? SocketType Stream, ProtocolType Tcp); // назначаем сокет локальной конечной точке и // слмпаеи входящие сокеты .л. •• w -V ir# ’ - ” х к мч Х4а аНС*1»' ~
1*16 Глава 4 stxstener.Listen(IO); * // Начинаем слушать соединения while (true) { Console.WriteLine ("Waiting for a connection on port 0", ipEndPoint); // программа приостанавливается, ожидая входящее соединение Socket handler * sListener. AcceptO; string data » null; // мы дождались клиента, пытающегося с нами соединиться while(true) byte[] bytes = new byte[1024j; int bytesRec - handler.Receive(bytes); data += Encoding.ASCII.GetStnng(bytes,O,bytesRec); if (data IndexOf(“<TheErd>") > -1) { i break; } // показываем данные на консоли Console.WriteLine("Text Received; 0”,data); string theReply = “Thank you for those + data.Length.ToString() + " characters. . . " byte[] msg = Encoding.ASCII.GetBytes(theReply);, handler.Send(msg); handler.ShutdownjSocketShutdown.Both); handler.Close() ; } } > • t catch(Exception e) { /' /.............' . Л Console. WriteLine(e.ToString()); \ < •* 1 : ' * ' . } // конец программы Hain Д / s, } Первый шаг заключается в установлении для сокета локальной конечной точки. Прежде чем открывать сокет для ожидания соединений, нужно подготовить для него адрес локальной конечной точки. Уникальный адрес для обслуживания TCP/IP определяется комбинацией IP-адреса хоста с номером порта обслуживания, которая создает конечную точку для обслуживания. Класс Dns предоставляет мето- ды, возвращающие информацию о сетевых адресах, поддерживаемых устройством в локальной сети. Если у устройства локальной сети имеется более одного сетевого адреса, класс Dns возвращает информацию обо всех сетевых адресах, и приложение должно выбрать из массива подходящий адрес для обслуживания. Создадим IPEndPoint для сервера, комбинируя первый IP-адрес хост-компьюте- ра, полученный от метода Dns. Resolve(), с номером порта.
Работа с сокетами 117 И устанавливаем для сокета локальную конечную точку IPHostEntry IpHost » Dns.ResolveC’localhost"); IPAddress ipAddr = ipHost. Add ressList[OJ; IPEndPoint ipEndPoint =? new IPEndPoint(ipAddr, 11000); Здесь класс IPEndPoint представляет localhost на порте 11000. Далее новым экземпляром класса Socket создаем потоковый сокет. Установив локальную конечную точку для ожидания соединений, можно создать сокет: И создаем сокет Tcp/ip Socket sListener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); Перечисление Add ressFamily указывает схемы адресации, которые экземпляр класса Socket может использовать для разрешения адреса. В следующей таблице представлено несколько важных параметров: Значение AddressFamily ’ Описание InterNetwork Адрес для IP версии 4 InterNetworkV6 Адрес для IP версии 6 Ipx Адрес IPX или SPX NetBios Адрес NetBIOS В параметре SocketType различаются сокеты TCP и UDP. В нем можно опреде- лить в том числе следующие значения: Значение SocketType Описание Dgram Поддерживает дейтаграммы. Значение Dgram требует указать Udp для типа протокола и InterNetwork в параметре семейства адресов. Raw Поддерживает доступ к базовому транспортному протоколу. Stream Поддерживает потоковые сокеты Значение Stream требует указать Тер для типа протокола и InterNetwork в параметре семейства адресов. Третий и последний параметр определяет тип протокола, требуемый для соке- та. В параметре Р rotocolType можно указать следующие наиболее важные значения: Значение ProtocolType Описание Raw Протокол необработанных пакетов. Tcp Transmission Control Protocol Udp User Datagram Protocol Ip Internet Protocol
118 Глава 4 «ь>«ял* >ХГ А-**~ WbftMM***; Wu*< iW'iWtW'l Otr^MiWwWiiW»JW M»WWM*I — *** Следующим шагом должно быть назначение сокета с помощью метода Bind(). Когда сокет открывается конструктором, ему не назначается имя, а только резерви- руется дескриптор. Для назначения имени сокету сервера вызывается метод Вind(). Чтобы сокет клиента мог идентифицировать потоковый сокет TCP, серверная про- грамма должна дать имя своему сокету. try { sListene г.Bind(i pEndPoint); Метод Bind() связывает сокет с локальной конечной точкой. Вызывать метод Bind() надо до любых попыток обращения к методам Listen() и AcceptO. Теперь, создав сокет и связав с ним имя, можно слушать входящие сообщения, воспользовавшись методом Listen(). В состоянии прослушивания сокет будет ожи- дать входящие попытки соединения. sListener.Listen(IO); В параметре определяется задел (backlog), указывающий максимальное число со- единений, ожидающих обработки в очереди. В приведенном коде значение парамет- ра допускаем накопление в очереди до десяти соединений. В состоянии прослушивания надо быть готовым дать согласие на соединение с клиентом, для чего используется метод AcceptO. С помощью этого метода получа- ется соединение клиента и завершается установление связи имен клиента и серве- ра. Метод AcceptO блокирует поток вызывающей программы до поступления соединения. Метод Accept () извлекает из очереди ожидающих запросов первый запрос на со- единение и создает для его обработки новый сокет. Хотя новый сокет создан, перво- начальный сокет продолжает слушать и может использоваться с многопоточной обработкой для приема нескольких запросов на соединение от клиентов. Никакое серверное приложение не должно закрывать слушающий сокет. Он должен продол- жать работать наряду с сокетами, созданными методом Accept для обработки входя- щих запросов клиентов. while (true) { Console WriteLine(“Waiting for a connection on port 0". ipEndPoint) ; 11 программа приостанавливается, ожидая входящее соединение Socket handler = sListener. AcceptO; Как только клиент и сервер установили между собой соединение, можно отправ- лять и получать сообщения, используя методы Send() и Receive() класса Socket. Метод Send () записывает исходящие данные сокету, с которым установлено сое- динение. Метод Receive() считывает входящие данные в потоковый сокет. При ис- пользовании системы, основанной на TCP, перед выполнением методов Send() и ReceiveO между сокетами должно быть установлено соединение. Точный прото- кол между двумя взаимодействующими сущностями должен быть определен забла- говременно, чтобы клиентское и серверное приложения не блокировали друг друга, не зная, кто должен отправить свои данные первым. string data = null; И мы дождались клиента, пытающегося с нами соединиться
Pat c .a с сокетами J19 while(true) { byte[] bytes = new byte [1024]; // данные, полученные от клаента int bytesRec = handler.Receive(bytes); // байты преобразуются в строку date += Encoding./SCII.G3tSt^lng(by+es,A bytesRec); // проверка конце lij мания IT (dal a. IndexOfC <TteEnd>") > -1) { break; } // показываем данные на консоли Console. WriteLine(“Text Received: 0",data); М<,тзд"есз1уе()яолучаетлин1< cr мм ti. cbtvlhj t г массив байтов, передан- ный в saRtCidt «ргулеяса. озв ащае_ю.‘ напей «е npyeinfi -пело фактически считан» ж сайтов. -В н р н виденном кода при получении данных в пре- • н Р«1 .ЮП.1ННОИ нр -М !IJ>< J..MH I | f | IIM!!OJ|,I I.OHtf | | и ч. I В г •| I ft я Если < ИМВОМЫ Конца сообщения в строке нс найдены, продолжается ожидание нходящих данных. иначе сообщение отображается на консоли. При выходе из цикла готовим новый массив байтов для ответа, ^спорый нужно послать клиенту. После преобразования вызываете. [ «етод Stnc X отправляющий string ^heReply = "“Thank you Tor those ” + data. Length ToStnngO + “ characters...”; byte[] msp « Encoding.ASCII. GetBytes(theReply); handler.Send(msg); Когда обмен давен «ми между еерверстги клиентом заканчивается, -закройте со- маявмййовв44вб1|мамаранвнраваам«^мвадмаиммкмавабрабаввмыыкжанных в«авмввввввввввж9мвфвжнниянвПввв<фо8втавь1зываитейШм1ав11(ХДавжажж>- гожжваа^ядвЕж^вамтлвваввмпв файвву, Жбив—е—дужию выпить метод Ciose(). handler nutdown(Socketshutdown Both); handler. CLoseO; SocketShutdown — это перечисление, содержащее три значения для остановки гпкета:---------- ---------------------------------------------- ——.------------------------------------------А, ----------------------------------------------Р.д.1^. ( Зм a to г -SocketShutdown Описание Both Останавливает отправку и получение данных сокетом. Receive Останавливает получение данных сокетом. ' Send Останавливает отправку данных сокетом.
120 Глава 4 Сокет закрывается при вызове метода Close(), который также устанавливает в свойстве Connected сокета значение false. Построение клиента на базе TCP Функции, которые используются для создания приложения-клиента, более или менее напоминают серверное приложение. Как и для сервера, используются те же методы для определения конечной точки, создания экземпляра сокета, отправки и получения данных и закрытия сокета. Вот полный код для SocketClient.cs и его объяснение: using System^ ’ .. с х using Systeih.Net.Sockets; . usi^g System.Net; - x .. using SystemiText; public class SpeketClient . , - public static void Main(string [J args) { // буфер для входящих данных byte[j bytes « new byte[lQ24J; // соединяемся с удаленным устройством // Устанавливаем удаленную конечную точку для Сокета IPHostEntry ipHost r Ons.Respl^(Mi27.S.p<F>; - IPAddress ipAddr - ipHost.AcfdressList[6]; z IPEndPoiht IpEndPoint = new TlQCiOk Socket sender = new Socket(AddtessFamily.InterNetwork,‘ 5^4 J SocketType^Sf ceam, .Proto^Type^Tcpi; Il Соединяем сокет с удаленной конечной точкой
Работа с сокетами ___121 senoer Connect(ipEndPoint), ? i Console. WritelineCSocket connected to {0}”. string neMessage » "TMs is $ t’est”^ >- ' l?yte[] msg = Encoding,ASCII. GeiByteo(theMessage+ 4TheEnd>’); // Отправляем данные через сокет int bytpsSent = sender.Send(msg); // Получаем отве1’ or удаленною устройства int byt0Rec =? sehder JIeceiVe(bytes)i: \ , Console *ritelineC‘Ther Server says : {0}% Encoding. ASCII. GetSt nng( bytes, 0, bytesRec)), Освобождаем сЪкеТ ' se^oer . Shutdc /’nCSocketShutdowi Both г. sender. CioseO” catch(Exception е) { < Console.Wrj 1 Единственный новый метод, метод Connect (), используется для соединения с уда- ленным сервером. Посмотрим, как он применяется в клиенте. Сначала нужно установить удаленную конечную точку: // Устанавливаем удаленную конечную точку для сокета IPHostEntry ipHost = Dns.Resolve(“127.0.0.Г); IPAddress ipAddr = ipHost.AddressList[O]; IPEndPoint ipEndPoint = new IPEndPoint(ipAddr, 11000); Socket sender = new Socket(AddressFamily.InterNetwork, SocketType.Stream,ProtocolType.Tcp) ; Теперь соединим наш сокет с удаленной конечной точкой: sende г.Connect(i pEndPoint); Получив сокет, метод Connect() устанавливает соединение между сокетом и уда- ленной точкой, заданной в параметре. Установив соединение, можем отправить наши данные и получить ответ: ) string theMessage = “This is a test”; byte[] msg = Encoding.ASCII.GetBytes(theMessage+"<TheEnd>”); // Отправляем данные через сокет int bytesSent = sender.Send(msg); // Получаем ответ от удаленного устройства int bytesRec sender.Receive(bytes);
122 Глава 4 Console.WriteLinef“The Server says : {0}’, Encoding.ASCII.GetString(bytes,0, bytesRec)): Наконец освободим сокет, вызывая метод Shutdown(), останавливающий отправ- ку и получение данных, и вызовем метод Close (): sende г.Shutdown(Socketshutdown.Both); sender. CloseO; На следующих снимках экрана показаны в действии сервер и клиент: Управление исключениями в System.Net.Sockets Когда в сети возникает какая-либо ошибка, класс Socket обычно порождает ис- ключение SocketException. Причиной могут послужить самые разные проблемы; рас- смотрим два случая — проблемы в конструкторе Socket и проблемы при соединении с портами. Конструктор класса Socket принимает три параметра: AddressFamily, SocketType и ProtocolType, порождая SocketException, если между параметрами обнаружены ка- кое-либо несоответствие или несовместимость. Например, исключение SocketException вызовет попытка создать экземпляр класса Socket со следующими параметрами: У 1 Socket sSockpt.f ne'w. Socket (Add ressFamily. InterNetworkV6, » £?. SocketType.Stream, yg&ff ProtocolType.Tcp)T.W^W^^»ft*?' ЖИйк» Когда порождается это исключение, его свойство Message (унаследованное от класса Exception) выглядит так: An address incompatible with the requested protocol was used (Используется адрес, не совместимый с заданным протоколом.) IP версии 6 не поддерживается в TCP, поэтому значение InterNetworkV6 парамет- ра Add ressFamily несовместимо со значением Tcp параметра ProtocolType. Также хорошим кандидатом на обработку исключения является метод Connect (), Он порождает исключение, если ему не удается установить соединение между ло- кальной и удаленной конечными точками, заданными в параметрах Connect(). По- пробуем это использовать.
Работа с сокетами 123 Программа сканирования портов Было рассмотрено самое простое приложение клиент-сервер, теперь с по- мощью исключения SocketException создадим нечто более интересное. В следующем примере создадим собственную программу сканирования портов, которая пытается соединиться с localhost по каждому порту, указанному в цикле — для демонстрации сканер просматривает первые 1024 порта. Сообщаем об успеш- ных соединениях, а если установить соединение не удается, перехватываем порож- даемое в этом случае исключение SocketException. Сканер портов может использоваться для получения списка открытых портов на вашем компьютере. В открытых портах проявляется потенциальная слабость сис- темы, которой могут воспользоваться приложения-нарушители. Вот полный код программы PortScanner. cs: J using System; using System.Net.Sockets; using System. Net; Э public class SocketConn , { . / public static void Maln(string [] args) IPAddress address « IPAddress. Parse(*127.C,0.T); for (Jnt4=1; i < 1024; i++) J Console.WriteLine(“Checking pprt {0}”, i) try { IPEndPoint endPoint = new IPEndPoint(address, Socket sSocket = new Socket(AddressFamiIy.InterNetwork, SocketType.Stream, ' " ProtocQliype.Tcp) ; . ‘s . ............ sSocket. Connect(endPoint ); Console.WriteLine(“Port {0} is listening",i>; catch(SocketException ignored) { ; - if (ignored ErrorCode != 10061) Console.Writeline(ignored Message); ) } Этот код пытается установить соединение с каждым портом, указанным в цикле. Если порт открыт, сокет устанавливает соединение и выводит строку с информаци- ей об этом порте. Если порт закрыт, порождается исключение SocketException. Ког- да исключение возникает, его свойство Message должно вернуть следующий текст: No connection could be made because the target machine actively refused it
124 Глава 4 утверждающий, что соединение с сервером установить нельзя. В классе Socket- Exception есть свойство ErrorCode, хранящее целочисленное значение, представ- ляющее последнюю возникшую ошибку операционной системы. Для исключения, порожденного при выполнении попытки соединения с закрытым портом, значение ErrorCode равно 10061. В блоке catch, обрабатывающем SocketException, проверяем, не отличается ли ErrorCode от этого значения, и отображаем сообщение об ошибке, чтобы дать читателю информацию о том, что случится при установлении соеди- нения: catch(SocketException ignored) { if (ignored.ErrorCode != 10061) Console.WriteLineCignored.Message) ; } Чтобы получить код ошибки, свойство ErrorCode вызывает основной метод GetLastError(). Список кодов ошибки можно найти в файле WinEr ror. h в следующей папке: Microsoft Visual Studio .NET\Vc7\PlatformSDK\Include Вот что получается в итоге на консоли, если запустим этот код на машине с Windows 2000 Server. Checking port 1 Checking port 2 Port 21 is listening Port 25 is listening Port 80 is listening Завершение соединений Код из блока finally будет выполняться независимо от порождаемых исключе- ний, и поэтому данный блок предоставляет идеальное место для вызовов методов Shutdown() и Close(). Это изменение гарантирует, что все сокеты будут закрыты до завершения программы. try { } catch( .. ) { finally nOpened.Connected) .vCketOpened. ShutdownCSockeXShutdpwn Both): socketOpened.CloseO: ' Г " ~ .МА Здесь используется свойство Connected, чтобы определить, открыт ли сокет. Сво- йство Connected возвращает состояние соединения сокета, и значение true указыва- ет, что сокет открыт. Если это так, тогда сокет закрывается.
Работа с сокетами 125 Опции сокетов Среда .NET обеспечивает методы SetSocketOption() и GetSocketOption(), позволя- ющие устанавливать и получать опции сокетов. Метод SetSocketOption(), в свою оче- редь, обращается к функции setsockopt Windows Socket API. Метод SetSocketOption() получает параметры SocketOptionLevel, SocketOptionName и значение для установки опции сокета — массив байтов, int или object. Перечисление SocketOptionLevel определяет уровень опции для сокета; когда устанавливаем или получаем опцию для сокета, это перечисление используется, чтобы указать, к какому уровню модели OSI она применима (устанавливается ли опция на уровне TCP, на уровне отдельного сокета и т. д.): Значение SocketOptionLevel Описание IP Опции применимы к сокетам IP Socket Опции применимы к самому сокету Tcp Опции применимы к сокетам TCP Udp Опции применимы к сокетам UDP Второй параметр, необходимый для метода SetSocketOptionsO, — это SocketOp- tionName; он определяет имя параметра, чье значение устанавливаем. Значения пе- речисления SocketOptionName можно найти в документации .NET Framework SDK; рассмотрим некоторые опции. ReuseAddress По умолчанию только один сокет может быть связан с локальным адресом, кото- рый уже используется. Однако может потребоваться связать с локальным адресом более одного сокета. Рассмотрим следующий код, пытающийся связать два сокета с одной и той же локальной конечной точкой: // Устанавливаем локальную конечную точку для сокета IPEndPoint IpEndPoint = new й• IPEndPoint(Dns.GetHostByName(Ons.GetHo$tName()).AddressList[OJ.11000); г // Создаем сокет Tcp/ip <.. > . Socket sListener = new Socket(AddressFamily.InterNetwork, SocxetType.Stream, ProtocolType.Tcp); | // Назначаем сокет локальной конечной точке и У. ’ Н еиуйэм входящие сркёты ‘iH’ . ' eListener. Bind (ipEndPoint);; sListener.Listen(10); Л // создаем новый сокет Socket sListener2 » new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); , ? // Назначаем сокет локальной конечной точке W' Слушаем, входящие 'со^ты; с
126 Глава 4 sLi stener2.Bind(i pEndPoint); sListener2.Listen(10); Попытка назначить два сокета одной конечной точке породит SocketException со следующим свойством Message: Only one usage of each socket address (protocol/network address/port) is normally permitted (Обычно каждый адрес сокета (протокол/сетевой адрес/лорт) разрешается использовать только один раз.) Решить эту проблему можно с помощью опции ReuseAddress. Используя эту опцию и ненулевое целое значение, можно разрешить для сокета несколько назна- чений адресов: sListener2.SetSocket0ption(Socket0ptionLevel.Socket. •. й SocketOptionName ReuseAddress. 1); ’: sListener2.Bind(IpEndPoint); ’4Й&-- > 4г- Обратите внимание на ненулевое целое значение — в действительности функ- ция setsockopt Windows Sockets API, которая вызывается методом SetSocketOption() интерпретирует его как логическое значение t rue. Время разъединения Через значение Linger параметра SocketOptionName можно определить, каким должно быть поведение сокета, когда в очереди на отправку остаются данные, а со- кет закрывается. Класс LingerOption содержит информацию о времени разъедине- ния сокета, некоторые важные члены этого класса перечислены далее: Свойство Описание Enabled Определяет, нужно ли медлить с разъединением после закрытия сокета. LingerTime Время (в секундах) разъединения, если свойство Enabled имеет значение true Если свойство Enabled имеет значение false, то сокет закроется сразу же, как только будет выполнен метод Close(). Если свойство Enabled имеет значение true, оставшиеся данные будут отправляться по назначению, пока время, заданное сво- йством LingerTime, не истечет. В последнем случае соединение будет закрыто при возникновении одного из двух событий: или время истечет, или все данные будут от- правлены. Заметьте, что, если данных в очереди нет, сокет закрывается сразу же не- зависимо от значения свойства Enabled. Кроме того, сокет закрывается немедленно, если в свойстве LingerTime задано значение 0. Эти два свойства можно также установить в конструкторе класса LingerOption. Первый параметр задает свойство Enabled, второй устанавливает свойство Lin- gerTime. Значение, которое хранит объект LingerOption, передается затем в метод SetSocketOption(), как показано далее. Заметьте явное приведение к типу object, не- обходимое для передачи объекта LingerOption: ft Создаем новый класс LingerOption и устанавливаем свойства в конструкторе , .LingerOption? l.ingerOpts = new LingerOption(true, 5); , 4 • mySocket,- SetSpcketOption(SocketoptionLevel. Socket? SocketOptionName.Linger, V: lobject)lihgerOpts); I *
Работа с сокетами 127 Продемонстрируем, как методом GetSocketOptionO получить значение Lin- gerTime. У метода GetSocketOption() есть три перегруженные версии, каждая из кото- рых возвращает свой тип данных — пустой тип, массив байтов или объект. Перегруженный метод, возвращающий object, требует только параметров SocketOp- tionLevel и SocketOptionName. Перегруженный метод, не возвращающий значения, заполняет массив байтов, который задается как дополнительный параметр. Пере- груженный метод, возвращающий байтовый массив, требует, чтобы в дополнитель- ном параметре int была задана длина данных, которые необходимо получить. Используем перегруженный метод, возвращающий объект; приведение объекта к типу LingerOption позволяет получить свойство LingerTime: у object о « mySocket.GetSocketOption(SocketOptionLevel.Socket, ' , 4? SocketOptionName.Linger); .. , I-*,*- <-. f ?»ЛЛ- '<* 'f T- • Console.WriteLine(((LingerOptlon)o).LingerTime); Асинхронное программирование Ha полное выполнение большинства функций работы с сокетами в исходной библиотеке Беркли требуется неопределенное время. В таких случаях функцию на- зывают блокированной. Это значит, что вызов функции может заблокировать теку- щий исполняемый поток, и, если у приложения есть только один поток, тогда все приложение будет ждать, пока функция вернет управление, и уже затем сможет про- должить выполнение. Например, функции send и receive называют блокированны- ми, поскольку они не возвращают управление немедленно. При асинхронном программировании соединение, ожидая завершение процес- сов в сети, не блокирует приложение. Вместо того чтобы блокировать текущий по- ток, оно использует асинхронную модель .NET для обработки сетевых соединений на другом потоке, в то время как приложение продолжает выполняться на первона- чальном потоке. Когда второй поток заканчивает выполнение, он посылает перво- начальному потоку объект события, указывающий состояние задачи. События могут быть сигнальными (signaled), в таком случае методы, ожидающие события, немедленно возвращают управление, или несигнальными (non-signaled). Если со- бытие несигнальное, ожидающий метод будет блокироваться, пока событие не ста- нет сигнальным. Существуют события двух типов—автоматические, которые, став сигнальными, сами устанавливают себя обратно в несигнальное состояние, и события с ручным сбросом, которые остаются сигнальными, пока не будут переведены в несигнальное состояние. В платформе .NET можно через класс ManualResetEvent указать програм- ме, что надо продолжить работу—установка объекта ManualResetEvent в сигнальное состояние прекратит блокирование любых ожидающих методов, и программа про- должит выполнение. Будем пользоваться этим объектом, чтобы облегчить асин- хронное программирование. Модель асинхронного программирования подходит для приложений, которые, не ожидая завершения сетевых операций, продолжают выполнение. В мире плат- формы .NET имеется полный набор методов для асинхронных операций. На- пример, в асинхронном программировании вместо обычного метода Send() используется метод BegihSend(). В асинхронной модели для возврата результата операции используется метод обратного вызова. В среде .NET Framework имеется набор методов для инициирова- ния задачи в асинхронной манере, но для завершения операции требуется функция обратного вызова. Например, в методе Connect () для начала соединения с сетевым устройством используется BeginConnectO, а чтобы завершить соединение, требуется соответствующая функция обратного вызова.
128 Глава 4 Среда .NET Framework обеспечивает класс-делегат для асинхронных операций. Этот класс, называемый AsyncCallback, обеспечивает для приложений способ завер- шения асинхронной операции. AsyncCallback aCallback = new AsyncCallback(AsyncCallback); Этот делегат передается как аргумент асинхронной функции при инициирова- нии операции. Указатель функции, на который ссылается AsyncCallback, содержит программную логику завершения обработки асинхронной задачи для клиента. Для обработки сетевых соединений в этой программной модели используется несколько потоков. Чтобы облегчить синхронизацию между разными потоками, используется класс ManualResetEvent, приостанавливающий выполнение основного класса и подающий сигнал, когда выполнение может быть продолжено. Начнем с построения асинхронного приложения-клиента и протестируем его при помощи сервера сокетов, построенного ранее. После этого завершим обсужде- ния асинхронного программирования созданием асинхронного сервера, который соединим с клиентом. Асинхронное приложение-клиент Следующая схема поможет понять последовательность выполнения асинхрон- ного приложения-клиента. Таким образом, нужно выполнить асинхронно три задачи: соединиться с серве- ром, отправить данные серверу и получить данные от сервера.
Работа с сокетами 129 Сначала обсудим, как организовать синхронизацию. Асинхронная природа кода требует синхронизации — нельзя работать с сокетом, если не закончено установле- ние соединения, поскольку вместо того, чтобы заняться отправкой сообщения, вы можете попытаться получить данные и вызовете взаимную блокировку. Для синхро- низации порождаемых потоков будем использовать класс ManualEventReset. Полный код для этого примера можно найти в файле ASyncClient. cs. public class AsyncClient •• \ ? 4 \ public static string. £tteRespon9fc«/Л?: 4 ~ '*! %! public static byte[] buffer = new byte[1024]; f public staticManualResetEvent GonnectDone w new ManualResetEvent (false); public static ManualResetEvent SendDone = new ManualResetEvent(false); “ public static ManualResetEvent ReceiveDone = new ManualResetEvent(false); Методы Set() и ResetO объекта ManualResetEvent могут установить или сбросить состояние объекта (установить его в сигнальное или несигнальное состояние). Каж- дую из трех задач — соединение, отправку и получение данных — снабдим объектом ManualResetEvent, а основной поток может ждать завершения соответствующей зада- чи, вызывая метод WaitOneO соответствующего объекта ManualResetEvent. Этот ме- тод блокирует текущий поток, пока объект не перейдет в сигнальное состояние. При создании объектов ManualResetEvent, как показано выше, указывается значение ( false, которое означает, что сначала состояние не установлено. Конечно, основной поток может продолжать обработку, но, прежде чем начинать новую задачу, убедим- ся, что предыдущая работа выполнена — как раз в этот момент появляется метод WaitOneO. Прежде чем рассматривать первую задачу, создадим и инициализируем сокет, который собираемся использовать. IPHostEntry ipHost » Dns.ResoIve(*‘i27.0,Cf.TT;b IPAddress ipAddr = ipHost.AddressList[O]; ' IPEndPoint enoPoint = new IPEndPoint(ipAddr. 11000); Socket sClient = new Socket(AddressFamily.Internetwork, “W ’ > ‘ 1 1 SocketType.Stream, ProtocolType.Top); Первая задача состоит в установлении соединения с сервером. Она решается с помощью метода BeginConnect (), который асинхронно начинает устанавливать сое- динение с удаленным хостом. Для него требуется метод обратного вызова, в кото- ром реализован представитель AsyncCallback. Метод обратного вызова должен вызвать метод EndConnect (), который завершит запрос на соединение и вернет сое- диненный сокет. sClient.BeginConnect(endPoint, new AsyncCallback(ConnectCallback), sClient); Этому методу передаются три параметра: первый параметр представляет уда- ленный хост с помощью класса IPEndPoint, второй—делегата AsyncCallback, который используется для передачи указателя на функцию, третий — это объект, содержа- щий информацию о состоянии, которая должна быть передана указанному методу обратного вызова. В данном случае передается экземпляр сокета. Рассмотрим метод ConnectCal IBack(): ^public static void ConhectCalIback(IAsyRcResylt |r), ’.f Ц Л , ’ Thread thr ^^readXurrentThread; 5 М» * th^Threadstate); ? ,
130 Глава 4 Прежде всего инспектируем поток, в котором выполняется асинхронный ме- тод. Информацию о потоке можно получить из свойства CurrentTh read объекта Th read. Она покажет, что код асинхронного метода выполняется в фоновом потоке. Далее узнаем, как получить объект Socket. Параметр lAsyncResult содержит ин- формацию о состоянии асинхронной операции. Пользуясь свойством AsyncState интерфейса lAsyncResult, можно извлечь аргумент, который был передан в третьем параметре метода BeginConnectO. Полученное значение нужно привести к типу Socket. Socket sClient « (Socket) мг< AsyncState; * , v 1 - $ й $ Теперь вызовем метод EndConnectO, чтобы завершить асинхронный запрос. Если при обращении к сокету произошли какие-либо ошибки, EndConnectO вызовет исключение Socket Except ion. sClient.EndConnect(ar); ; S "v , .,•*» m' « -к г-'Ж ' Console.WriteLine("Socket connected to (Of’, J ' . j. s I £ sClient. RemoteEndPoint.ToStringO); Наконец вызовем метод Set() класса Manual Reset Event для объекта ConnectDone — тем самым сообщим основному потоку, что завершили установление соединения. Connectbone,Set(); 7 > V V 4 4 < -<-• . " - "... J/-. 1 ’ Соединившись с сервером, приступаем к следующему шагу — взаимодействию с сервером через отправку и получение данных. Метод BeginSend() используется, чтобы асинхронно запустить отправку данных соединенному сокету. sClient.BeginSend(byteData,0, byteData.Length, 0, f, : new AsyncCallback(SendCallback),sCllent); " Первый параметр — это массив байтов, содержащий данные для отправки, вто- рой представляет позицию в буфере, от которой нужно начать посылать данные, третий параметр — это размер буфера, предпоследний—делегат AsyncCallback и по- следний используется для сохранения информации о состоянии. Метод BeginSend() вызывает функцию обратного вызова, переданную посредст- вом делегата AsyncCallback: public static void SendCallb^ck(lAsyncResult ar) S j , - Thread thr = Thread CurrentThread; • 4 j Л Console.WriteLine("SendCallback Thread Stated + th r.Threadstate)f > ' Socket sClient = (Socket)ar,AsyncState, int bytesSent = sClient.EndSend(ar); Console.WriteLineC"Sent {0} bytes to server/', bytesSent); SendDone.$et(); Это такой же код, как в функции ConnectCallback(), не считая того, что он вызы- вает метод EndSendO, заканчивающий асинхронный запрос на отправку данных, и устанавливает ManualResetEvent для SendDone. Метод BeginReceive() запускает асинхронное получение данных от сокета*
Работа с сокетами 131 sClient. BeginReceive(buffer, 0, buffer ..Length, 0, new AsyncCallback(ReceiveCallback), sClient); Аргументы этого метода такие же, как в методе BeginSend(). Чтобы вернуть дан- ные, считанные из сокета, метод обратного вызова должен использовать EndReceive(). public static' void ReceiyeCaliback(IAsyncResult ar) 'Threadthr -Thread. Cur rentThread; - й A v , - ; Console; Wri teLine (*ReceiveCallback Th read State: ” + th r =. Th readState); Socket sClient « (Socket) ar AsyncState; - -Ал?. ' int bytesRead = sClient. EndReceive(ar); ? У _ Й.Г.Р < УУ< if (bytesRead > 0) theResponse += Encoding ASCII.GetString(buffee, 0, bytesRead); r sClient.BeginReceive(buffer, 0, bufferLength. 0, 1 new A$yncCallback(ReceiveCallback), sClient); else ч ReceiveDone Set(): V?-, ' . , ^^-5.. >‘ Чтобы гарантировать получение всех данных, внутри обратного вызова вызыва- ется метод BeginReceive(). Метод EndReceive() возвращает число полученных байтов, поэтому проверим, остались ли еще данные в очереди. Полученные данные сохра- няются в строковом поле theResponse. Рассмотрим метод Main(), который связывает все воедино — отдельные части этого кода мы уже видели. Начнем с отображения информации о текущем потоке, так мы сможем различать поток, в котором действует сокет, и основной поток. За- тем создадим сокет, как уже делали раньше: public Static void Hain<strina [ 1 arg) j . ':{ - - / ч' i ' Л,, J.- „ . - У'-уу " v.Vi... ;’ '-Vu -J XL А Thread thr = Thread.CurrentThread; Console.WriteLine(“Main thread State:” + thr.Threadstate); IPHostEntry ipHost = Dns.Resolve(“127.0.0.1”) ; IPAddress ipAddr = ipHost.AddressList[O]; IPEndPoint endPoint = new IPEndPoint(ipAddr, 11000); Socket sClient = new Socket(AddressFamily.InterNetwork. SocketType.St ream,ProtocolType.Tcp); Теперь можно приступить к установлению соединения. Вызываем метод Begin- Connect (), указав вызов ConnectCallBack(), и, прежде чем посылать сообщение соеди- ненному сокету, ждем завершения операции. Основной поток блокируется методом ConnectDone.WaitOne() — это значит, что переход к отправке данных не осуществится, пока соединение не будет установлено при помощи метода Set () объекта Connect- Done, выполняющегося в ConnectCallBack().
132 Глава 4 sClient.BeginConnect(endPoint, new AsyncCallback(ConnectCallback), sClient); GonnectDone .Wai tOne<); Определим сообщение, которое пошлем серверу. Чтобы сообщение было под- линнее, создадим его в цикле конкатенацией строк. На примере такого сообщения будет проще понять, как действует асинхронная операция. string data = “This is a test.”; V for (int i«0; i<72; i++) « data +- i.ToStringd + f (new string(‘= i)); byte£] byteData « Encoding.ASCII.GetBytes(data + “<TheEnd>”); Теперь можно начать асинхронную отправку данных — сообщение подготовле- но и соединение установлено: sClient.BeginSend(byteData, 0, byteData Length, О, new AsyncCallback(SendCallback), sClient); Для иллюстрации действительно асинхронной работы нашего клиента “выпол- ним некоторую другую обработку”. Здесь в цикле просто переводим текущий поток Thread в ожидание на одну сотую долю секунды—этот прием дает подходящую ими- тацию вычислений, пожирающих процессорное время. for i=0; i<5; i++) j f // Выполняем некоторую другую обработку- .. л Console,Writeixnefv; • ® '* <• ? \ Thread. Sleep(10>; '; Очевидно, что перед получением данных от сервера, все данные должны быть отправлены. Поэтому йспользуем метод SendDone. WaitOne() и блокируем поток, пока SendCallBack() с помощью SendDone.Set() не подаст сигнал об окончании отправки данных. // Прежде чем подучать данные,- надо все отправить . :;х t i SendDone. WaitOneO^ ** « *-••'-** ’ sClient.BeginReceive(buffer,0, buffer.Length, О, new AsyncCallback(ReceiveCallback),sClient); Наконец ждем получения данных, отображаем их, останавливаем и закрываем ReceiveDone. WaitOne(); Console.WriteLine(“Response received: {0} ”, theResponse); sClient.Shutdown(SocketShutdown.Both); sClient.Closed; catch(Exception e) f c ( ’< % J*- ' Console. Write Line (e .ToString ());
Работа с сокетами 133 Вывод на консоль асинхронного клиента, когда он запускается вместе с (син- хронным) сервером сокета, разработанным ранее, имеет следующий вид: Асинхронное приложение-сервер Последовательность операций в асинхронном приложении-сервере демонстри- руется на следующей схеме:
134 _____ ______________________________ ___________Глава 4 Сервер начинает работу с создания сокета и прослушивания порта, с которым этот сокет связан. Для асинхронного приема соединений асинхронному приложе- нию-серверу нужно пользоваться методом BeginAccept(), а после установления сое- динения для получения данных от сокета клиента и отправки данных клиенту в сервере используются методы BeginReceive() и BeginSend(). После вызова BeginAccept() устанавливаем событие в состояние ожидания, что- бы другой поток приложения мог выполняться, пока клиент пытается установить соединение. Если это не сделать, то из-за асинхронной природы сервера приложе- ние завершится прежде клиента. Для установки и ожидания событий используем класс ManualResetEvent: AsyncCallback aCallback = new AsyncCallback(AcceptCallback), // sListener - это экземпляр класса Socket sListener.BeginAccept(aCallback, sListener); // предположим, что socketEvent - это экземпляр класса ManualResetEvent socketEvent.WaitOne() ; Метод BeginAccept() принимает два параметра: асинхронный делегат, ссылаю- щийся на метод обратного вызова, и параметр, используемый для передачи данных функции обратного вызова (в данном случае передается сам слушающий сокет). Ког- да сокет получает новое соединение, метод BeginAccept() вызывает функцию AcceptCallback(). Она вызывает метод EndAccept (), который возвращает новый сокет для взаимодействия с клиентом. В асинхронном программировании сокетов используются потоки для разных асинхронных операций. Задача основного потока состоит в инициировании слуша- ющего сокета, еще один поток занят приемом входящих запросов, а другой — при- емом и отправкой данных. Идентификатор текущего потока можно получить с помощью функции GetCurrentThreadId() класса AppDomain; это число отображается на консоли, поэтому видно, что разные операции выполняются в разных потоках: Console.WriteLine(“Main Thread ID: " + AppDomain. GetCurrentThreadldO); Заметьте, что не требуется запускать отдельные потоки явно — это делает среда .NET Framework. После того как с помощью метода EndAccept () согласие на новое соединение дано, новый сокет может взаимодействовать с клиентом через вызовы асинхрон- ных методов BeginReceive() и BeginSend(): Socket handler = listener.EndAccept(ar); handler.BeginReceive(buffer, 0, buffer.Length, 0, new AsyncCallback(ReceiveCallback), handler); Функция ReceiveCallbackO сначала вызывает функцию EndReceiveO, завершаю- щую ожидающую асинхронную задачу. Затем ищем в данных символ конца сообще- ния. Если этот символ найден, то для отправки данных обратно клиенту используем метод BeginSend(), иначе вызываем BeginReceive(), чтобы получить оставшиеся дан- ные: int bytesRead = handler.EndReceive(ar); if (bytesRead >0) { !
Работа с сокетами 135 content += Encoding.ASCII GetString(buffer, 0, bytesRead); if (content.IndexOf(“.") > -1) < Console.WriteLine("Read {0} bytes-from socket. \ n Data: {1}”, content.Length, content); byte[] byteData = Encoding.ASCII.GetBytes(content); handler.BeginSend(byteData, 0, byteData.Length, 0, new AsyncCallback(SendCallback) , handler); } else handler.BeginReceive(buffer, 0, buffer.Length, 0, new AsyncCallback(ReceiveCallback), handler); } Метод SendCallbackO завершает операцию, вызывая EndSendO, закрывает сокет handler и устанавливает ManualResetEvent для основного потока, чтобы приложение смогло продолжить работу: int bytesSent = handler.EndSend(ar); Console.WriteLine(“Sent {0} bytes to Client.”, bytesSent); handler.Shutdown(Socketshutdown Both); handler. CloseO; socketEvent.SetO; Далее приводится полный исходный код программы AsyncServer.cs: using System, ; jsing System.Net.Sockets using System.Net; using System.Text; . ? using System .Threading; public class ASyncServer 1 хл . .. // буфер для получений и отправки данных public static byte(] buffer = new byte[i024);| // класс события для поддержки синхронизации public static ManualResetEvent socketEvent = new ManualResetEvent(false); public static void Main(string [] args.) { ‘ j Console.WriteLine(“Main Thread ID:? + AppOomain.GetCurrentThreadldQ); byte[] bytes » new byte[10241; IPHostEntry ipHost = Dns.Resolve(Dns.GetHostName()); 4 _ IPAddress ipAddr = ipHost, AddressList[01; ..............................................X. X'-. , Л’ЛЛ л IPEndPolnt localEnd* = new IPEndPoint(ipAddr, 11000), Л- . - . ‘ ..... Socket sListener new Socket(AddressFamily,Internetwork. C- SocketType.Stream.
Глава 4 ProtocolType.Тер); Ц связываем сокет sListener.Bind(locaiEnd); ' /7 начинаем слушать sListeneг. Listen(10); Console.WriteLinef‘'Waiting for a connection... AsyncCallback aCallback * new AsyncCallbacktAcceotCa^lback); 77 асинхронны функция, дающая согласие на соединения sListener.BeginAcoept(aCalIback,sListener); ” /7 *де*|, пока другие потоки закончат паботу socketEvent WaitOneO; " }. public static void AcceptCalloackQAsyncResult ar) Console.WriteLLnel"AcceptCalloack Thread ID:” + AppDomain.GetCurrentThreadied)); 7/ юсе для получен:!я запросов Socke-. listener - (SocketJar.AsyncState; Ц новый1 сокет • Socket handler = listener.EndAccept(ar); handler.BeginReceive(buffer, 0, buffer.Length, C, ' new AsyncCallback(ReceiveCalIback), ha r'die r); public static void ReceiveCallback(lAsyncfiesult ar) i • . .... / Console.WriteLine(“ReceiveCallback Thread ID:” * AppDomain.GetCurrentThreadId()) ; string content » String.Empty Socket handled = (SpdWi). ar AsyncState; . ' ' '••I ’ 'fiy. int bytesRead'» hanoler.EHdReceive(ar); < // если данные есть... , ’’"TS’ if (bytesRead > 0) чм • ' vxi; - .{ . г ; •• ; // присоединяем их к основной строке content Encoding.ASCII.GetStringCbuffer, 0^ bytesRead); /7 если, ш находим еймйол конца сообщения л.... •*. it (content.IndexQf> -14 Console.Writeiihel'"Reaj {0} bytes from socket. Kn Data: {1}"., тЛ content.length, content); j/ • 4.: .r bytef] byteData ® Encoding. ASCH. Get Bytes (con tent X 77 отправляем данные обратно клиенту ‘ ’ f handIeT;Beg.in^nd(DyteDsfta, Q, ByteData Length 5 . } new; AsyncCallback(SendCalloack), handler); else
Работа с сокетами 137 4- :: Л A:! А .-'•‘U лнач$ nq/^аен оставдие^я данные .- -- > ,;i handler.BeginRecei.e(buffer, 0, buffer lengtn, o, / new AsyncCallback(ReceiveCallback), handler); , « ) ;' ‘ ’ ‘ S’' - ! ' ", . Г z _ public static void SendCallbackflAsyncResult ar) { . .jjj -.; Console.WriteLine(“SendCallback Thread ID:" + AppDomain.GetCurrentThreadId());. Socket handler = (Socket) ar.AsyncState, .,// отправляем данные обратно клиенту 4nt bytesSent- handler. EndSend(ar); Console.WriteLine(“Sent {0} bytes to Clientrf bytesSent) // закрываем сокет ?•, . -,4, • handler. Shut down (Socketshutdown. Both); handler. CloseO; ‘ 11 устанавливаем событие для основного потока | BbcketWdt/Set(); ? ’• • " Разрешения сокетов Среда .NET Framework предоставляет многочисленные классы, помогающие разрабатывать в приложениях безопасный код. Многие из этих классов предлагают основанную на ролях безопасность и криптографию. Среда .NET Framework также дает объекты разрешений доступа к коду, являющиеся компоновочными блоками для защиты от несанкционированного доступа к коду. Они являются фундаментом, гарантирующим управляемому коду безопасность — может выполняться только тот код, который имеет разрешение выполняться в текущем контексте. Каждое разрешение доступа к коду демонстрирует одно из следующих прав: □ Право доступа к защищенным ресурсам, например к файлам □ Право выполнения защищенной операции, например обращения к управляе- мому коду Для мира Интернета и особенно для сетевых приложений классы System. Net предоставляют встроенную поддержку аутентификации и разрешений доступа к коду. Среда .NET Framework дает класс SocketPermission, обеспечивающий соблю- дение разрешений доступа к коду. Класс SocketPe rmission используется для управления правами на установление и принятие соединений, контролирующими доступ к сети через сокеты. Этот класс состоит из спецификации хоста и набора “действий”, определяющих способы уста- новления соединений с этим хостом. Он обеспечивает безопасность кода, контро- лируя значения имени хоста, IP-адреса и транспортного протокола. Для сокетов C# имеется два способа обеспечить разрешение, поддерживающее безопасность: □ Императивно, используя класс SocketPermission □ Декларативно, используя SocketPe rmissionAttribute
138 Глава 4 Синтаксис императивной безопасности реализует разрешения, создавая новый экземпляр класса SocketPermission, чтобы при выполнении кода запросить конкрет- ное разрешение, например право установить TCP-соединение. Этот способ обычно применяется, когда настройки безопасности изменяются при выполнении прило- жения. В декларативном синтаксисе используются атрибуты, позволяющие помес- тить информацию о безопасности в метаданные нашего кода, чтобы клиент, вызывающий код, мог воспользоваться отражением (reflection) и узнать, какие раз- Императвная безопасность Этот синтаксис » w ют гп тгггггстг* создает новый экземпляр класса SocketPermission. Синтаксис императивной безопасности можно использовать для выполнения требований и переопределений, но-недли запросов. Прежде чем вы- звать соответствующий -критерий безопасности, необходимо через конструктор инициализироват ь состояние класса SocketPermission,-чтобы он прсдстаьлял кон- кретную форму разрешения, которую вы ищете. Эта разновидность синтаксиса безо астюсп г аолезна, когдавыобладае ге информацией, необходимой для безопас- ности и доступной лишь при выполнении, например, если через порт надо защи- тить каупм-дмбо жпгт, но до цмщцмсмм» нртрлммы ни пим хгмта.ди нлмгрпорта неизвестны. В следующем приложении демонстрируется основное использование класса SocketPermission. Поскольку этот код ведет себя как клиент, до выполнения этой npoq гммы нужно запустить приложение SocketServer .cs, созданное ранее в этой гпяяе Инчче при метода, угтанавнивающего соединение, будет порождено \ исключение SooketExceptгопЛрсграмма принимает.изжпмандной строки. в аобяза- тельный параметр, который может при нимиti значение assert (поумолчанию, если -опция не задана) или deny. Используем эту опцию, чтобы предоставить разрешение или отказать в разрешении программе, пытающейся установить соединение с сер- вером. using System; . к .. ( ' 'Г.,*- v, using System, Net. Sockets; using System Net; using aSiebBe^Security; using System SecurПу.Hermissions; M > public class LmpSecurity V. { ' -y-: '.." - - "" « public static void Main(String [] arg) .. . Э & „ - ' // из командной строки можно передать опций assert иди deny // если опция - assert, то программа выполняется успешно // иначе порождается исключение securityexception String option = null; “ . ъ/' ф - & • J# * :Y* v .-Л/Л. An- t£‘." else if (arg,Length > 0) } option в arg[O] option « “assert”;f
Работа с сокетами 139 Console.WriteLineГoption+ option): . < ’’ ' MethodA(option); X =• ‘ 5 x ? ,} \ 4 x . ММЙ? . Jfe&IL- public static void MethodA(String option) ,( 4. . Д-r Console.WriteLine!“MethodA"); *'t. > . < < »*, >. M IPHostEntry IpHost^ Dns.Resolve(“127.0.0.T ); IPAddress ipAddr * ipHost Addre$sList[O); IPEndPoint ipEndPoint - new IPEndPoint{ipAddr, 11000):; Socket sender * new Socket(AddressFamily.InterNetwork, SocketType.Stream.ProtocolType.Tcp); $; • SocketPermission permitsocket = new 7? ’> SocketPermission(NetworkAccess.Connect, s TransportType.Tcp, “127.0.0.1", SocketPermission.AllPorts); г - f 1 й „ // на основе переданного параметра выбираем Assert или Deny ; if (opt ion. tquals("deny”)) pe rmitSOcket.Deny().; else ' > , { : permitsocket AssertO; r . , ' < * - ‘8 ' r- » try LF •. r'" . кМИ { - • 4- . // соединяем сокет с удаленной endPoint. перехватываем все ошибки?^ sender.Connect(IpEndPoint^ 5 ’ • - . Console.WriteLineC'Socket connected to {0}”, sender.RemoteEndPoint.ToStringO); . - new byte[1024]; t ... 5M|«- j-™ " *. * #* fes Encoding.ASCII•GetBytes(“This // отправляем данные через сокет int bytesSent « sender.Send(msg); oytefl oytes byte(J m§g - *^,ЯГЙ is a test<E0E>“); к // получаем ответ от удаленного устройства int bytesRec « sender.Receive(bytes); O>; Console WriteLineCEchoed Test = (0)”, '* > Encoding.ASCII.GetString(bytes,0, bytesRec)); catch(SocketException se) { Console.WriteLine(“Socket Exception;- I ' S r ? catch(SecurltyException sece) .| Console .Writ eLine (“Socket ExceptionO + sece.ToStnngQ); E ж- ” ’ u> finally. { "«EL. if (sender. Connected) ; > . ЙИВ .« . >
440 Глава 4 й Li . ix t-r // освобождаем сокег sender.Shutdown(SocketShutdown floth); V sender, Closet); i s£; ,д } -r *• г -ж iF Console.writetineC'Closing MetnodA"); / | 1 J'. % , rr } t • OjJ Приведенный код показывает, как, используя императивный синтаксис, реали- зовать безопасность доступа к коду. Важность этого кода в данном случае заключает- ся в вызове одного метода класса SocketPemission: Assert() или Deny(). По существу программа следует по двум разным путям в зависимости от аргумента, переданного в командной строке. Один путь демонстрирует успешное установление соединения, а второй иллюс- трирует неудачу, вызванную привлечением кода к поддержке безопасности. В пока- занной программе код ограничен в установлении соединений TCP с сервером. Это ограничение выполняется вызовом метода Deny() класса SocketPermission. Но перед вызовом любого метода этого класса надо инициализировать объект SocketPer- mission. Эта инициализация выполняется в конструкторе: SocketPermission permitsocket = new SocketPermission(NetworkAccess. Connect, TransportType.Tcp, “127.0.0.1", SocketPermission.AllPorts); Э1от конструктор создает новый экземпляр классаSocketPermission и инициали- зирует его для заданного транспортного адреса указанным разрешением. Парамет- ры обозначают доступ к сети (в данном случае соединение с сервером), разрешение на который предоставляется, тип транспортного протокола (TCP), имя хоста и но- мер порта. NetworkAccess может принимать одно из следующих значений: Имя члена Описание Accept Указывает, что приложению разрешается давать согласие на запросы соединения от Интернета и местного ресурса. Connect Указывает, что приложению разрешается устанавливать соединения с определенными ресурсами Интернета. Параметр TransportType представляет транспортный протокол, он может при- нимать значения АП, Тер, Udp и т. д. Третий параметр — это имя хоста, а послед- ний — номер порта. После выполнения приведенной выше строки создается новый объект Soc- ketPermission, контролирующий доступ к указанному имени хоста и порту с исполь- зованием заданного Т ransportTypfe. Выполнив таким образом инициализацию объекта SocketPermission, можно вы- звать метод Asserts) или метод Deny(), чтобы разрешить или запретить коду выпол- нение указанного NetworkAccess. Передача этому приложению параметра “deny” (т. е. выполнение приложения вводом из командной строки Impsecurity deny) приводит к появлению на консоли следующего вывода: C:\Networking\Sockets> ImpSecurity deny option:deny MethodA
Работа с сокетами 141 Socket Exception:System.Security.SecurityException: Request for the permission of type System.Net.SocketPermission, System, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 failed. at System.Security.SecurityRuntime.FrameDescHelper (FrameSecurityDescriptor secDesc, IPermission demand. PermissionToken permToken) at System.Security.CodeAccessSecurityEngine.Check(PermissionToken permToken, CodeAccessPemission demand,StackCrawlMark& stackMark,Int32 checkFrames, Int32 urirestrictedOverride) at System Security.CodeAccessSecurityEngine.Check(CodeAccessPermission cap, StackCrawlMark& stackMark) at System.Security.CodeAccessPermission.Demand() at System.Net.Sockets.Socket.CheckCacheRemote(SocketAddress socketAddress, EndPoint remoteEP, Boolean isOverwrite) at System.Net.Sockets.Socket.Connect(EndPoint remoteEP) at ImpSecurity.MethodA(String option) The state of the failed permission was: ciPermission class="System.Net.SocketPermission, System, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1"> <ConnectAccess> <ENDP0INT host="127.0.0.1” transport=”Tcp” port="11000"/> </ConnectAccess> </IPermission> Closing MethodA Декларативная безопасность Декларативная безопасность использует атрибуты .NET, чтобы поместить ин- формацию о безопасности внутрь метаданных кода. Атрибуты можно поместить на уровне сборки, класса или члена и указать необходимый тип запроса, требования или переопределения. Для использования этого синтаксиса безопасности сначала через декларативный синтаксис нужно инициализировать данные состояния объек- та SocketPermissionAttribute, чтобы он представлял форму разрешения, соблюдение которого обеспечивается в коде. В следующем примере демонстрируется, как обеспечить выполнение разреше- ния с использованием SocketPermissionAttribute. Определяем два метода — LegalMethod() и IllegalMethod(), — каждый из которых пытается установить соедине- ние с сервером через TCP. В первом методе объект SocketPermissionAttribute предоставляет необходимое разрешение, во втором методе атрибут запрещает сое- динение: •,Ч£ using System; using System.Net.Sockets; using System. Net; Using System.Text; using System.Security; using System.Security. Permissions; public class DecSecurity * public static void Main(String И arg) 4 Ik' • • f*' tegalMethod(>; HlegalMethodQ ; ? К/' W* Ж?
142 Глава 4 (SocketPermission(SecurityAction.Assert, Access т ‘‘Connect”, Host » “127.0.0.1”,, Port = “All",1'Transport = “Tcp")] public static void LegalMethod() < Console.WriteLine(“LegalMethod"); IPHostEntry ipHost » Dns. Resol ve( “127.0.О.Г): W IPAddress ipAddr » ipHost.AddressList[O]; H IPEndPoint ipEndPoint « new IPEndPoint(ipAddr. 11000); Socket sender » new Socket(AddressFamily.InterNetwork, SocketType.Stream, X3₽i^tocdnV0d>Tcpj; try // соединяем сокет с удаленной endPoint и перехватываем все ошибки sender.Connect(ipEndPoint); Console.WriteLine(“Socket connected to {0} % sender.RemoteEndPoint.ToStringQ) }- ** - 16 ' *'• Й catch(SecurityException se) Console.WriteLine(“Seenrity Exception:“ * se); } ><-. •. • - - catch(SocketException se) - < . . Console.WriteLinef“Socket Exception.” + se) ' b . :v; '/<- . finally . ... • if (sender.Connected) {X ' ''I // освобождаем сокет sender.Shutdown(SocketShutdown.Both) sender.Closed- I Тер”)] [SocketPermission(SecurityAction.Oeny, Access = “Connect”r Host = “127.0.0.1”, Port = “All", Transport public static void IlIegalMethodQ Console. WriteLine( “IllegalMethod”); IPHostEntry ipHost =? Dns.Hesolve(“127.0.0.1"); IPAddress ipAddr =? ipHost.AddressUst[0); JfiEndPoint ipEndPoint « newIPEndPoi^ntii ipAddr» 11000); ,;Л Sockst sender » V- ,?;Ж=? ‘Hi -S' -Й5Г >> 1 - "J a ^ew Souket(AodressFaniily. Internet worn, SocketType.'Stream, ProtoCoIType.Tcp); r V r • ...... . К try {' .......................................................... Z/ соединяем сокет с удаленной endPoint и перехватываем все ошибки sonder.Connect(ipEhdPoint)i. .. Console.WriteLine(“Socket connected to {D}.”,1 catch(SocketException Й ro^jTyoStnngd): ’*? ь п-^Ж ж
Работа с сокетами 143 По функциональности приведенная выше программа похожа «а предыдущую. Однажо я последней прпгршмг пшггтммпцмц—В безопасности используется синтаксис декларативной безопасности. Ее код содержит два разных метода сое- динения с сервером, поскольку атрибутами нелтдя танину ифодазъдиыаминески. ^Етм—е, —п[11 in магамамм-нанноапь. тиэютцдпгя юбмиагпие Socket- PermissionAttribute: [SocketPermission(SecurityAction.Deny, Access = “Connect", Host = “127.0.0 Г, Port = "All", Transport = “Tcp")] В приведенном выше коде объявление и инициализация SocketPermissionAtt ri- bute очень напоминает конструктор объекта SocketPermission. Свойства Socket- PermissionAttribute не должны быть ссылками null или некорректными значениями. Кроме того, эти свойства, установленные однажды, не могут изменять- ся. Значения свойств такие же, как и для класса SocketPermission. Откомпилирован- ный код декларации сохраняется внутри метаданных кода приложения, поэтому вызывающий код может использовать отражение, чтобы узнать, какие разрешения _-ярябувмиьдлмыиаммммка«мгиж£}ла. Итоги В этой главе мы рассмотрели разработку клиентских и серверных приложений с использованием класса Socket. При этом также обсудили: □ Что такое сокет и как сокеты введены в операционную систему. □ Два основных типа сокетов: потоковые и дейтаграммные сокеты. □ Еще один тип сокета, используемый для специализированного низкоуровне- вого программирования, называемый сырым сокетом. □ Вся поддержка сокетов обеспечивается классом System.Net.Sockets.Socket. □ Управление исключениями для сетевых приложений обеспечивается клас- сом SocketException. □ Методы Close() и Shutdown(), как правило, должны помещаться внутрь блока finally. □ Основные опции сокетов и как их установить и получить, используя методы SetSocketOption()и GetSocketOption().
144 Глава 4 jLWin-.rfrrf.-хггтглпплп-.тт- г т.чг..rniitr irr лтп1шпптаптптттгтгп—и-nwirririrrr—i-n—rfiTTmr i i" 1. ~1.. — -'i —- — ............................................... ...x.-.- □ Асинхронную модель программирования в .NET и применили ее к построе- нию асинхронного базирующегося на сокетах приложения клиент-сервер. □ Использование делегата AsyncCallback для обеспечения функций обратного вызова для асинхронной обработки. □ Использование класса ManualResetEvent для синхронизации разных потоков в асинхронном приложении. □ Декларативный и императивный синтаксис безопасности, обеспечивающий безопасность на уровне кода в сетевых приложениях. □ Императивный синтаксис следует использовать, когда необходимые разре- шения становятся известны при выполнении, если же разрешения известны при компиляции, нужно использовать декларативную безопасность. Следующую главу мы посвятим TCP и узнаем, что предусмотрено в среде .NET Framework для работы с этим важным протоколом.
ГЛАВА 5 I TCP 13 предыдущей главе было рассмотрено низкоуровневое программирование сокетов Для выполнения задач, связанных с сетями. В данной главе детально разбе- рем высокоуровневые сетевые классы, предоставляемые средой .NET Framework. После общего обзора протокола TCP, его архитектуры и структур данных исследуем классы TcpClient и TcpListener, имеющиеся в среде .NET Framework для работы с TCP, и наконец обсудим использование канала TCP в среде .NET Remoting. Обзор TCP TCP, или Transmission Control Protocol, используется как надежный протокол, обеспечивающий взаимодействие через взаимосвязанную сеть компьютеров. TCP проверяет, что данные доставляются по назначению и правильно. TCP — это ориентированный на соединения протокол, предназначенный для обеспечения надежной передачи данных между процессами, выполняемыми или на одном и том же компьютере или на разных компьютерах. Термин “ориентирован- ный на соединения” означает, что два процесса или приложения прежде чем обме- ниваться какими-либо данными должны установить TCP-соединение. В этом TCP отличается от протокола UDP (который будет рассмотрен в следующей главе), явля- ющегося протоколом “без организации соединения”, позволяющим выполнять ши- ро ювещательную передачу данных неопределенному числу клиентов. Инкапсуляция Когда приложение отправляет данные, используя TCP, они перемещаются вниз по стеку протоколов. Данные проходят по всем уровням и в конце концов передают- ся через сеть как поток битов. Каждый уровень в наборе протоколов TCP/IP добавляет к данным некоторую информацию в форме заголовков и/или концевиков: Когда пакет прибывает на конечный узел в сети, он снова проходит через все уровни снизу доверху. Каждый уровень проверяет данные, отделяя от пакета свою информацию в заголовке и/или концевике, и наконец данные достигают серверно- го приложения в той же самой форме, в какой они покинули приложение-клиент.
146 .......................................................... Глава 5 Терминология TCP Прежде чем рассматривать, как TCP устанавливает соединение с другим хостом TCP, приведем несколько терминов, которые необходимо определить. Сегмент Порция данных, которую TCP отправляет IP, называется сегментом TCP. Дейтаграмма Порция данных, которую IP отправляет уровню сетевого интерфейса, называет- ся дейтаграммой IP. Порядковый номер Каждый сегмент TCP, отправленный через соединение, имеет назначенное ему число, которое называется “порядковым номером” (sequence number). Оно исполь- зуется, чтобы гарантировать прибытие данных в правильном порядке. Заголовки TCP Чтобы понять, как работает TCP, вкратце рассмотрим структуру заголовка TCP: Порт-источник Порт-адресат Порядковый номер Номер подтверждения Смещение р । Биты данных - гезвРв । управления : Окно Контрольная сумма Опции Указатель срочности Заполнение Порядковые номера и номера подтверждений используются TCP, чтобы гаран- тировать, что все данные прибывают в правильном порядке, а биты управления г I к
TCP 147 содержат разнообразные флаги, указывающие статус данных. Таких битов управле- ния (обычно представляемых трехбуквенными сокращениями) всего шесть: □ URG — указывает, что сегмент содержит срочные данные. О АСК — указывает, что сегмент содержит номер подтверждения. □ PSH — указывает, что данные нужно протолкнуть к получающему пользовате- лю. □ RST — сбрасывает соединение. □ SYN — используется для синхронизации порядковых номеров. □ FIN—указывает конец данных. Соединения TCP Для установления соединения TCP использует процесс, называемый “трехфаз- ным квитированием” (Three-Phase Handshake). Как следует из названия, этот про- цесс включает три шага: 1 • Клиент инициирует взаимодействие с сервером, посылая сегмент с установленным битом SYN. Этот сегмент содержит начальный порядковый номер клиента. 2. Сервер отвечает отправкой сегмента с установленными битами SYN и АСК. Этот сегмент содержит начальный порядковый номер сервера (не связанный с порядковым номером клиента) и номер подтверждения, на единицу больший порядкового номера клиента (т.е. равный следующему порядковому номеру, ожидаемому от клиента). 3. Клиент должен подтвердить этот сегмент отправкой обратно сегмента с установленным битом АСК. Номер подтверждения будет на единицу больше порядкового номера сервера, а порядковый номер будет равен номеру подтверждения сервера (т. е. на единицу больше начального порядкового номера клиента).
148 лава 5 Операции TCP Теперь, узнав в общих чертах, как TCP устанавливает соединения, рассмотрим немного подробнее несколько операций TCP, чтобы понять, как TCP передает дан- ные. Обзор потоковой передачи данных Как видно, TCP передает данные порциями, которые называются сегментами. Чтобы гарантировать правильное и в должном порядке получение сегментов, каж- дому из них назначается порядковый номер. Получатель отправляет подтвержде- ние получения сегмента. Если подтверждение не получено до истечения интервала тайм-аута, данные отправляются еще раз. . Каждому октету (восьми битам) данных назначается порядковый номер. Поряд- ковый номер сегмента равен порядковому номеру первого октета данных в сегмен- те, и это число отправляется в заголовке TCP данного сегмента. В сегменте может также присутствовать номер подтверждения, равный порядковому номеру следую- щего ожидаемого сегмента данных: TCP использует порядковые номера, чтобы гарантировать, что дублирующие данные получающему приложению переданы не будут и данные будут доставлены в правильном порядке. Заголовок TCP содержит контрольную сумму, чтобы гаран- тировать корректность данных при доставке. Если получен сегмент с неверной кон- трольной суммой, он просто отбрасывается, и подтверждение не отправляется. Это означает, что, когда значение тайм-аута истечет, отправитель повторит передачу сегмента. Управление потоком TCP управляет объемом направляемых ему данных, возвращая с каждым под- тверждением “размер окна”. “Окно” — это объем данных, который может принять получатель. Между прикладной программой и потоком данных в сети располагается буфер данных. “Размер окна” фактически представляет собой разность между раз- мером буфера и объемом сохраненных в нем данных. Это число отправляется в заго- ловке, чтобы информировать удаленный хост о текущем размере окна. Такой прием
TCP 149 называется “раздвижным или скользящим окном” (“Sliding Window”). Алгоритмы раздвижного окна управляют потоком данных, передаваемых в сети: Полученные данные сохраняются в этом буфере, и приложение может обра- щаться к буферу и считывать из него данные со свойственной ему скоростью. По мере того как приложение считывает данные, буфер опустошается и может прини- мать следующие данные, поступающие из сети. Если приложение считывает данные из буфера слишком медленно, размер окна падает до нуля, и удаленный хост получает команду прекратить передачу данных. Как только локальное приложение обработает данные в буфере, размер окна воз- растает и поступление данных из сети возобновляется. Если размер окна больше размера пакета, отправитель знает, что получатель мо- жет хранить одновременно несколько пакетов, что повышает производительность. Мультиплексирование TCP дает возможность нескольким процессам на одной машине одновременно использовать сокет TCP. Сокет TCP состоит их адреса хоста и уникального номера порта, а. TCP-соединение включает два сокета на разных концах сети. Порт может использоваться для нескольких соединений одновременно — один сокет на одном конце может использоваться для нескольких соединений с разными сокетами на другом конце. Примером этой ситуации служит Web-сервер, слушающий на порту 80 и отвечающий на запросы от нескольких компьютеров. { Введение в TCP на платформе .NET Поддержка сокетов TCP на платформе .NET значительно усовершенствована по сравнению с предыдущей моделью программирования. Раньше большинство раз- работчиков, использовавших Visual C++, для реализации любых типов взаимо- действия сокетов обращались к классам CSocket и CAsyncSocket или пользовались библиотеками независимых поставщиков. Для высокоуровневого программирова- ния TCP встроенная поддержка практически отсутствовала. В .NET для работы с сокетами предоставлено особое пространство имен System.Net.Sockets (которое обсуждалось в предыдущей главе). Это пространство имен содержит не только та- кие низкоуровневые классы, как Socket, но и классы высокого уровня TcpCIient и TcpListener, предлагающие простые интерфейсы для взаимодействия через TCP. В отличие от класса Socket, в котором для отправки и получения данных приме- няется побайтовый подход, классы TcpCIient и TcpListener придерживаются потоко-
150 Глава 5 вой модели. В этих классах все взаимодействие между клиентом и сервером базируется на потоке с использованием класса Networkstream. Однако при необходи- мости можно работать с байтами. Класс TcpClient Класс TcpClient обеспечивает TCP-сервисы для соединений на стороне клиента. Он построен на классе Socket и обеспечивает TCP-сервисы на более высоком уровне—в классе TcpClient есть закрытый объект данных m_ClientSocket, используе- мый для взаимодействия с сервером TCP. Класс TcpClient предоставляет простые методы для соединения через сеть с другим приложением сокетов, отправки ему данных и получения данных от него. Наиболее важные члены класса TcpClient пере- числены далее: Открытые свойства Имя Тип Описание LingerState LingerOption / Устанавливает или возвращает объект LingerOption, содержащий информацию о том. будет ли соединение оставаться открытым после закрытия сокета и как долго. NoDelay bool Указывает, будет ли сокет задерживать отправку и получение данных, если буфер, назначенный для отправки или получения данных, не заполнен. Если свойство имеет значение false, TCP задержит отправку пакета, пока не будет накоплен достаточный объем данных. Это средство помогает избежать неэффективной отправки через сеть слишком маленьких пакетов. ReceiveBufferSize I int Задает размер буфера для входящих данных (в байтах). Это свойство используется при считывании данных из сокета. Receive!imeout int 1 Задает время в миллисекундах, которое TcpClient будет ждать получения данных после инициирования этой операции Если это время истечет, а данные не будут получены, возникнет исключение SocketException. SendBufferSize int Задает размер буфера для исходящих данных. SendTimeout int Задает время в миллисекундах, которое TcpClient будет ждать подтверждения числа байтов, отправленных удаленному хосту от базового сокета. При истечении времени SendTimeout порождается исключение SocketException.
TCP 151 Открытые методы Имя Описание Close() Закрывает ТСР-соединение. Connect() Соединяется с удаленным хостом TCP. GetStream() Возвращает объект Networkstream, используемый для передачи данных между клиентом и удаленным хостом Защищенные свойства Имя Тип Описание Active bool Указывает, есть ли активное соединение с удаленным хостом. Client Socket Задает базовый объект Socket, используемый объектом TcpClient. Поскольку это защищенное свойство, к базовому сокету можно обращаться, если вы производите ващ класс от TcpClient. Создание экземпляра класса TcpClient В классе TcpClient существуют три перегруженных конструктора: public TcpClientO; public TcpClient(IPEndPoint ipEnd); public TcpClient(string hostname, int port); Конструктор, используемый по умолчанию, инициализирует экземпляр TcpClient: TcpClient newClient = new TcpClientO; Если экземпляр TcpClient создается так, то для установления соединения с уда- ленным хостом надо вызвать метод Connect (). Второй перегруженный конструктор принимает один параметр типа IPEndPoint. Он инициализирует новый экземпляр класса TcpClient, связанный с указанной ко- нечной точкой. Заметьте, что это не удаленная, а локальная конечная точка. Если попытаться передать конструктору удаленную конечную точку, будет порождено ис- ключение, означающее, что в данном контексте IP-адрес задан некорректно. Если использовать этот конструктор, то после создания объекта TcpClient все-таки нужно вызвать метод Connect(): // Создаем локальную конечную точку IPAodress ipAddr - IPAddress Parse(“192.168.1.51"); IPEndPoint endPoirit = new IPEndPoint(ipAddr, 11100 fl TcpClient newClient - new TcpClient(endPoint); « * <7 Для создания соединения с сервером надо вызвать connect() ^newClient Connect(“l92.1J58.1 52", 11000), ...
152 Глава 5 Параметр, переданный конструктору объекта TcpClient, является локальной ко- нечной точкой, в то время как метод Connect () фактически соединяет клиента с сер- вером и поэтому принимает в качестве параметра удаленную конечную точку. Последний перегруженный конструктор создает новый экземпляр класса TcpClient и устанавливает удаленное соединение с использованием в параметрах DNS-имени и номера порта: TcpClient newClient - new TcpClient(“localhost”, 80); Это самый удобный метод, он позволяет инициализировать TcpClient, разре- шить DNS-имя и соединиться с хостом в одном простом шаге. Однако заметьте, что с помощью этого конструктора нельзя задать локальный порт, с которым желатель- но связаться. Установка соединения с хостом Создав экземпляр класса TcpClient, следующим шагом установим соединение с удаленным хостом. Для соединения клиента с хостом TCP предоставлен метод Connect(). Если для создания экземпляра TcpClient использовать конструктор по умолчанию или локальную конечную точку, то останется лишь вызвать этот метод, иначе, если конструктору были переданы имя хоста и номер порта, попытка вызова метода Connect () породит исключение. , Суп&стярагс три перегруженных метода Connect (): public void Connect(IPEndPoint endPoint); public void Connect(IPAddress ipAddr, int port); public void Connect(string hostname, int poYt); Они достаточно просты, но, тем не менее, на коротких примерах продемон- стрируем использование каждого перегруженного метода: 1. Передача объекта IPEndPoint, представляющего удаленную конечную точку, с которой надо соединиться: -7 Создаем новой экземпляр класса FcpClient TcpClierij newClient » new TcpClient(); // Устанавливаем соединение c IPEndPoint IPAddress ipAadr = IPAddress. Parse(“127.0 0.ГХ, ,.v IPEndPoint endPoint = new IPEndPoint(ioAddr, 80); // Соединяемся с хосюм через IPEndPbirft newClient Connect(endPoint); 2. Передача объекта IPAddress и номера порта: //Создаём новый экземпляр класса TcpClient TcpClient newClient « new TcoClientO; • £4 ' • X '.XiXh Xh 7/ Устанавливаем соединение c IPEndPoint x . IPAddress ipAddr « IPAddress. Parse(“127.0.0.1” ); ,;j, // Соединяемся с хостом, используя IPAddress и номер порта newClient Connect(ipAddr, 80); ,У ’ 3. Передача имени хоста и номера порта: // Создаем новий экземпляр класса IcpClieni TcpClient newClient * new TcpClientO, ...4 ’ .a • / r'v. i- Ei-ji
TCP 153 s // Соединяемся с хостом, используя строку с,именем хоста я порт newClient. Connect(“127.0.0.1“, 80) ; ч - Если соединение будет неудачным или возникнут другие проблемы, порождает- ся исключение SocketException: try { . . TcpCIient newClient * new TcpClientQ; , . ' . // Соединяемся с сервером newClient.Connect("192.168-0.1", 801; // В этот момент сокет порождает // исключение, если при соединении // возникают проблемы catch(SocketExceptюл se) / \ •' - - Console.WriteLine(“Exception: < * se) ; Отправка и получение сообщений Для обработки на уровне потока как канал между двумя соединенными приложе- ниями используется класс Networkstream. Это уже обсуждалось в главе 2, поэтому здесь только рассмотрим, как он используется с TcpCIient. Прежде чем отправлять и получать любые данные, нужно определить базовый поток. Класс TcpCIient предоставляет метод GetSt ream() исключительно для этих це- лей. С помощью базового сокета он создает экземпляр класса Netwo rkSt ream и возвра- щает его вызывающей программе. Следующий пример кода демонстрирует, как получить сетевой поток через метод GetStream(). Предположим, что newClient — это экземпляр TcpCIient, а соединение с хостом уже установлено. Иначе будет порождено исключение InvalidOperation. NetworkStream tcpStream s newClient.Getstream(); Получив поток, используем методы Read() и Write() класса NetworkSt ream для чте- ния из приложения хоста и записи к нему. Метод Write() принимает три параметра: массив байтов, содержащий данные, которые надо отправить хосту, позицию в потоке, с которой хотим начать запись, и длину данных: <' byte£] sendBytes = Encoding.ASCII.GetBytes(“This is a Test<E0F>”h tcpStream.Write(sendBytes, 0. sendBytes. Length); Метод Read () имеет точно такой же набор параметров—массив байтов для сохра- нения данных, которые считываются из потока, позицию начала считывания и чис- ло считываемых байтов: oyte[] bytes = new byte[newClient.ReceiveBufferSize], -• 1; / int bytesRead = tcpStream Read(bytes, 0, newClient.ReceiveBufferSize); // Преобразуем данные в строку . ~ « // returnData будет содержать все подтупившие данные от сокета string returnData « Encoding.ASCII.GetString(bytes); Свойство ReceiveBuffe rSize класса TcpCIient позволяет получить или установить размер (в байтах) буфера для чтения, поэтому используем его как размер массива байтов. Заметьте, что, устанавливая это свойство, мы не Ограничиваем число бай- тов, которое можно считывать каждой операцией, поскольку при необходимости
154 Глава 5 размер буфера будет динамически изменяться, но если задать размер буфера, это со- кратит накладные расходы. Закрытие сокета TCP После взаимодействия с клиентом, чтобы освободить все ресурсы, следует вы- звать метод Close (): Закрываем клиентский сокет. ' newClient.Closet); Вот и все, что нужно, чтобы использовать класс TcpClient для взаимодействия с сервером. Помимо этой основной функциональности имеются другие возможности. Если требуется обратиться к экземпляру сокета, базовому для объекта TcpClient, напри- мер для установки опций методом SetSocketOptionO, можно через свойство Client получить доступ к членам соответствующего объекта Socket. Можно также использо- вать свойство Client, чтобы сделать существующий объект Socket базовым сокетом для объекта TcpClient. Но поскольку это защищенный член класса TcpClient, прежде чем его использовать, наш класс должен наследовать классу TcpClient. Свойство Client дает возможность защищенного доступа к закрытому члену m_ClientSocket, о котором упоминалось ранее. Класс TcpClient передает сделанные на нем вызовы аналогичному методу класса Socket после проверки параметров и инициализации экземпляра сокета. Объект m_ClientSocket создается в конструкторе, который вы- зывает закрытый метод initialize(), строящий новый объект Socket, и затем вызы- вает метод set_Client(), чтобы назначить его свойству Client. Этот метод также устанавливает булево значение m_Active, используемое для отслеживания состояния экземпляра Socket. Он также проверяет наличие излишних соединений объекта Socket и операций, требующих установления соединения. Отдельное защищенное свойство Active предназначено для установки и получения значения закрытого чле- на m_Active. У сокетов есть масса опций, которые класс TcpClient не охватывает. Если нужно установить или получить какое-либо из этих свойств, не представленных в TcpClient (например, Broadcast или KeepAlive), необходимо произвести класс от TcpClient и использовать его член Client. В следующем коде демонстрируется, как можно использовать эти защищенные свойства, в данном случае используется член Active класса TcpClient: using System; , / using System.Net; ’ using System. Net.Sockets using System.IO; using System.Text; public class PSocket i TcpClient public PSocket() : base() »л м • ... .ч rri ’ : base(ipaddress. port) с» - , ? > a ййш.....; public PSocket(string ipaddress, int port) public static vpid Main() / •< . .-S’- -<{4 X . . • - ' .*..•« ЛИ- // Создание объекта TcpClient без соединения
TCP 155 szl ...» PSocket ps2« new PSocketO; <>. J .: .fc- Ъ > -*? . -x >x>_. Vf 8х=<» *>» S’ *= ^<»K-s4^'-4f 7^’* “ *" -i£ • *<« •» ‘ 44 // Вывод состояния сокета • Console.Writeltne(“trackActive: ” + ps2Active); * ff Соединение с клиентом ps2.Connect(“T27,6.O. V, 11000); - X If Проверка состояния сокета ?r/ Console.WriteLine(“trackActive: “ + ps2 .Active) ; ff Создание другого класса TcpCIient, на этот раз с соединением // внутри конструктора PSocket newClient = new PSocket(“127.0.0.1’4 11000); // Проверка состояния нового сокета . ? Console.WriteLineC’trackActive: ” ♦ newClient.Active:); * // Получение внутреннего защищенного члена сокета Socket s = newClient. Client; ‘ t'. V.’-« . S' // Использование сокета для установки опции О ... .ж»"' ‘ > s.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, 1); - -'r- ' ’ • - '• y ” ' Построение реального приложения на сокетах Чтобы продемонстрировать использование класса TcpCIient, построим простое приложение-клиент электронной почты, реализовав два протокола, наиболее рас- пространенных в мире Интернета: протоколы SMTP и POP3. Поскольку еще одна реализация POP3 представлена в главе 9, в данной главе не станем ее показывать, но можем скопировать с сервера вместе с кодом других программ для этой книги. Итак, начнем с некоторых теоретических положений, на которых базируется протокол SMTP, а затем перейдем к рассмотрению работа приложения. Здесь представим только основные сведения, достаточные, чтобы понять приложение, а более под- робно сделаем это в главе 9. Платформа .NET, конечно, дает классы для отправки электронной почта через SMTP (см. главу 9), но поскольку этот протокол довольно прост, он послужит полезным примером, на котором можно понять, как реализуют- ся протоколы прикладного уровня, подобные SMTP. Simple Mail Transfer Protocol (SMTP) На следующем рисунке изображена базовая модель протокола SMTP, используе- мая при выполнении почтовой транзакции:
456 '.................... Глава 5 Процесс начинается с отправления пользователем почтового сообщения. Для установления канала взаимодействия система-отправитель связывается с систе- мой-получателем через порт 25 ГСР. Система-получатель, которая может быть конечным пунктом назначения или промежуточной системой, откликается сообще- ниями состояния, информирующими отправителя, что она готова получить сообщение. Эти ответы представляют собой трехразрядные коды состояния и сооб- щений, удобные длй восприятия человеком. Вслед за первоначальным сообщением электронной почты система-отправитель посылает команды. Система-получатель отвечает соответствующими сообщениями состояния и командами. Связь заканчи- вается, когда отправитель завершает отправку сообщений и отсоединяется от сис- темы-получателя. Команды SMTP Синтаксис команд протокола SMTP совсем несложен, он содержит очень мало команд: □ НЕЮ. Команда HELO используется для инициирования взаимодействия между хостом-отправителем и хостом-получателем. Эту команду сопровождает па- раметр, идентифицирующий хост-отправитель. Хост-получатель в свою очередь идентифицирует себя отправителю, и обе машины переходят в со- стояние готовности к началу взаимодействия. В случае успеха на эту команду должен поступить ответ с кодом 250, указывающий, что команда завершилась без ошибки. Пример команды HELO и ответа от хоста мог бы выглядеть так: Система-отправитель: НЕЮ wrox.com Система-получатель: 250 gg.mail.com □ MAIL. Из этой команды Получатель узнает, кто отправляет почтовое сообще- ние, чтобы о любых ошибках в доставке почты можно было уведомить исход- ного отправителя сообщения. Поскольку на маршруте почта может перехватываться несколькими хостами, эта информация используется для идентификации конечного пункта назначения электронной почты. Боль- шинство почтовых серверов налагает на этот параметр некоторые огра- ничения, например, требуя гарантированного совпадения имени домена пользователя с доменом почтового сервера, чтобы предотвратить аноним- ные почтовые отправления и спаминг (рассылка по электронной почте ин- формации, не нужной адресату, например рекламного характера. Прим, науч, редактора.). Возможная команда MAIL и ответ: Система-отправитель: MAIL FROM:<noman@csquareonline.com> Система-получатель: 250 OK □ RCPT. Эта команда используется для отправки имен почтовых ящиков пользо- вателей, для которых посылается почта. Для конкретного почтового сообще- ния может указываться несколько адресатов. Идентификация адресата почтового сообщения Система-отправитель: RCPT ТО:<noman@csquareonline.com> Система-получатель: 250 0К
TCP 157 O DATA. Эта команда указывает, что следующая информация содержит основную часть файла сообщения. Конец сообщения задается системой-отправителем передачей строки, содержащей только символ точки (.). Следующий листинг показывает транзакцию между отправителем и получателем. Отметьте, что тема почтового сообщения задается в этих данных строкой SUBJECT:, непос- редственно следующей за командой DATA. Текст между SUBJECT: и символами пе- ревода каретки/возврата строки составляет тему сообщения: Система-отправитель: DATA Система-получатель: 354 Ready to receive data... Система-отправитель: SUBJECT:Subject of the message<CRxLF>This is a test message.<CR><LF>.<CR><LF> Система-получатель: 250 OK Получив строку конца сообщения, система-получатель отвечает отправителю состоянием сообщения. □ QUIT. Эта команда указывает, что система-отправитель готова закрыть канал связи между отправителем и получателем: Система-отправитель: QUIT Система-получатель: 221 web.akros.net closing connection. Пример SMTP-взаимодействия Далее вы обнаружите пример успешного сеанса SMTP между системами отпра- вителя и получателя. Во всех командах и ответах CLIENT представляет систему, ко- торая будет инициировать взаимодействие и отправлять почту, a SERVER — систему, которой направляется почта: [Establish Connection with SMTP Server] Connection Established with xyz.com SERVER: 220 webl xyz.com ESMTP SendMail CLIENT. HELO xyz com SERVER: 250 OK CLIENT: MAIL FROM: <noman@csquareonline.com><CR><LF> SERVER: 250 <noman@csquareonline.com>... Sender OK CLIENT: RCPT TO: <noman@csquareonline.com><CRXL^> SERVER: 250 <noman@csquareonline com>... Recipient OK CLIENT: DATA<CR><LF> SERVER: 354 Enter mail, end with on a line by itself CLIENT: SUBJECT: test<CR><LF> test<CR><LF> .<CR><LF> SERVER: 250 asdkauy83 Message accepted for delivery CLIENT: QUIT<CRXLF> SERVER: 221 web.xyz.com Closing Connection Чтобы опробовать этот пример, можете использовать приложение Telnet и сое- диниться с вашим локальным SMTP-сервером через порт 25. Затем вручную введите клиентскую часть примера в приложение Telnet, чтобы лучше почувствовать этот процесс. Рассмотрим учебный SMTP-сеанс в Telnet в главе 9, а о клиенте Microsoft Telnet см. в главе 1.
158 Глава 5 SMTP-клиент в .NET Приложение-клиент электронной почты будет Windows-приложением, но поль- зовательский интерфейс постараемся сделать по возможности очень простым. У нас будут однострочные текстовые поля, чтобы задать SMTP-сервер, с которым хотим соединиться, отправителя и получателя почты (поля From и То) и тему сооб- щения. Также введем многострочное текстовое поле для основной части сообще- ния и поле списка, где будем отображать сообщения состояния: У формы есть две закладки: одна предназначена для отправки сообщений элек- тронной почты через SMTP, а вторая — для получения их из ящика через POP. За- кладка SMTP содержит следующие элементы управления (показываем текст меток, связанных с элементами управления, чтобы их можно было распознать на показан- ном выше снимке экрана): Имя Тип Текст метки txtSmtpServer текстовое поле Smtp Server: txtFrom текстовое поле From: txtTo текстовое поле То: txtSubject текстовое поле Subject: IstLog поле списка Status Messages: txtMessage текстовое поле Message to Send: btnSend кнопка - Весь код соединения с сервером и отправки сообщения реализован в обработчи- ке события щелчка кнопки Send. Начнем с создания соединения с SMTP-сервером:
TCP 159 private void btnSend_Click(object sender. System.EventArgs e) // Создаем экземпляр класса TcpClient ’ . TcpClierit smtpServer = new TcpClient(tXtSmtpServer.Text, 25); IstLog.Items.Add(“Connection Established with smtpserver.com*’); На следующем шаге строятся классы Stream для взаимодействия с SMTP-серво ром. Используем NetworkSt ream для записи потока на сервер, а для считывания с сер- вера — StreamReader. Последний класс дает метод ReadLine(), пользоваться которым гораздо легче, чем вызывать метод Read() класса NetworkSt ream, считывающий поток в байты, что потребовало бы преобразования в строку с привлечением класса Encoding. Метод ReadLineO класса StreamReader сразу возвращает строку. /7 Создаем классу потоков для взаимодействия Networkstream writeStream * smtpServer.GetStream(); . ' j StreamReader readStream = new StreamReader(smtpServer GetSt reamO); Как только соединимся с сервером, он сообщит, что соединение установлено. Считываем это сообщение методом StreamReader.ReadLineO и отобразим в поле списка: // Извлекаем сообщение об успешном соединении receiveData - readStream. ReadLineO; ъ ; // Добавляем его к полю списка . т. IstLog.Items.Add(receiveData);, . Далее отправляем серверу почтовый адрес пользователя, записывая SMTP- команду MAIL FROM в объект Networkstream. Сервер опять ответит сообщением, кото- рое получим из объекта St reamReader и добавим в поле списка: // Отсылаем почтовый адрес отправителя sendString = “MAIL FROM:” + + txtFrom.Text + “>\r\n”; dataToSend Encoding.ASCII.GetBytesCsendString); \ . writeStream.Write(dataToSend, 0, dataToSend. Length); // Отображаем ответное сообщение X. receiveData readStream.BeadLineC)Г “ X > IstLog.Items.Add(receiveData); :: - Затем в команде RCPT ТО отправим почтовый адрес назначения и снова отобра- зим ответ сервера: // Отправка почтового адреса получателя sendString * “RCPT ТО: ” + •'<“ + txtTo.Text + L /X.; dataToSend # Encoding.ASCII,GetBytes(sendString); i^writeStream.Write(dataToSend, 0, dataToSend Length); X // Отображаем ответное сообщение receiveData = readStream. ReadLineO; IstLog.Items.Add(receiveData); Теперь, после того как оба почтовых адреса установлены, вслед за SMTP-коман- дой DATA посылаются сами данные (включая тему почтового сообщения): // Отправляем данные " ' J sendString = “DATA " + u\r\n”; • _ dataToSend « Encoding.A$CII.GetBytes(sendString); : writeStream.Write(dataToSend, 0, dataToSend.Length); 4 ' - ’ * "t.., ‘ 'J // Отображаем ответное сообщение -Ж receiveData ~ readStream. ReadLineO;
160 Глава 5 IstLog.Items.Add(receiveData); % 7 ' V44-','. >M ' ' M'.' ' ;« ft Отправка темы и ^текста сообщения .. *4^'г '-• sendString = ^‘SUBJECT: *' + txtSubject.Text + “\r\n*’ + txtMessage.Text W + V + “\r\n”; -г ; dataToSeno » Encoding.ASCII.GetBytes(sendString); writeStream. Write(dataToSend, 0, dataToSend.Length); receiveData = readstream. ReadLineO; v IstLog.Items.Add(receiveData); На последнем шаге остается отправить серверу команду QUIT и освободить все ресурсы, использовавшиеся приложением: // Отправляем Серверу сообщение разрыва соединения sendString == "QUIT “ * “\An”; dataToSend = Encoding.ASCII.GetBytes(sendString) ; writeStceam.Write(dataToSend, 0, dataToSend.Length); receiveData - readstream.ReadLineO; 1st Log.Iterns.Add(receiveData); •'I ». Z:' " • ' // Закрываем все открытые ресурсы с , writeStream.Close(); ‘ readstream. CloseO; smtpServer.Close(); > ц. j Post Office Protocol (POP) Вторая закладка нашего почтового приложения-клиента позволяет считывать почту из почтового ящика, используя протокол POP3: Полный код этого приложения можно скопировать со страницы нашего сайта, посвященной этой книге, кроме того, очень похожий пример приводится в главе 9, поэтому здесь этот код не показывается.
TCP 161 Реализация класса FtpWebRequest Второй пример, который мы рассмотрим, немного сложнее. Реализуем классы FtpWebRequest и FtpWebResponse, которые позволяют копировать файлы с FTP-серве- ра и на FTP-сервер почти таким же способом, каким можно обращаться к файлам че- рез классы FileWebRequest и FileWebResponse, рассмотренные в главе 3. Обзор FTP File Transfer Protocol (FTP) — это, как было сказано в главе 1, протокол приклад- ного уровня, построенный поверх протокола транспортного уровня, обычно по- верх TCP. Он используется для копирования файлов с удаленного сервера и на удаленный сервер. Во многих отношениях реализация FTP-клиента очень похожа на реализацию SMTP-клиента из предыдущего раздела — открывается соединение TCP с сервером, отправляются текстовые команды для выполнения таких дейст- вий, как извлечение файла с сервера, и сервер возвращает трехразрядный код (вместе с сообщением, удобным для восприятия человеком), чтобы показать состо- яние запрошенного действия. ' Однако FTP отличается от SMTP, в нем используются два разных соединения — управляющее соединение, на котором посылаются команды и получаются ответы сервера, и соединение для данных, используемое для самой передачи файлов с сервера или на сервер. По умолчанию сервер слушает команды от клиента на пор- ту 21, а когда нужно отправлять данные, открывает второе соединение с портом 20 клиента. Активный и пассивный режимы Соединение для данных открывается, если только отправлена команда копи- рования файла. В активном режиме (который установлен по умолчанию) клиент должен слушать соединения. Когда потребуется отправить данные, FTP-сервер от- кроет соединение с этим сокетом и передаст данные к клиенту. При таком подходе проблема заключается в том, что большинство конфигура- ций брандмауэров не позволит извне установить соединения с машинами за бран- дмауэром, а разрешит только те соединения, которые были инициализированы из-за брандмауэра. В FTP эта проблема решается вводом пассивного режима. В этом случае клиент отправляет команду, указывающую, что используется пассивный ре- жим, и на нее сервер отвечает номером порта, на котором он слушает. Когда нужно отправить данные, вместо того чтобы слушать запрос от сервера, клиент может от- крыть соединение с указанным портом. В нашей реализации будет использоваться пассивный режим. FTP-команды Спецификация FTP определяет несколько команд для аутентификации, копи- рования файлов на сервер и с сервера и изменения каталога на сервере. В нашем коде не будем пользоваться всеми командами, поэтому рассмотрим только те, кото- рые нам понадобятся: Команда Описание USER <имя лользователя> Имя пользователя, которое нужно удостоверить на сервере PASS <пароль> Пароль, связанный с именем пользователя
162 ~ Глава 5 , . _ , продолжение таблиц ы RETR <имя файла> Скопировать с сервера указанный файл STOR <имя файла> Скопировать файл на сервер и сохранить его в указанном месте ТУРЕ синдикатор типа> Формат данных. Может принимать одно значение из следующего перечня: - A—ASCII - Е —EBCDIC - 1 — Изображение (двоичные данные) > 1 L <размер байта>—Локальный размер байта PASV Использовать пассивный режим STAT Понуждает сервер отправить сообщение состояния клиенту. Команда может использоваться в ходе передачи данных, чтобы указать состояние операции. QUIT Закрывает соединение с сервером Коды состояния FTP Как и в случае кодов ответа SMTP, трехразрядные коды FTP упорядочены в соот- ветствии со степенью детализации, предоставленной разрядом кода: в первом раз- ряде дается общее указание о состоянии команды; второй разряд указывает общий тип возникшей ошибки; в третьем разряде содержится более конкретная информа- ция. Первый разряд может принимать следующие значения: Значение Описание 1 Положительный предварительный ответ—запрошенное действие инициировано, и, прежде чем клиент сможет послать новую команду, будет отправлен еще один ответ. 2 Положительный окончательный ответ—запрошенное действие завершено. 3 Положительный промежуточный ответ—команда принята, но сервер, прежде чем приступить к выполнению, должен получить дополнительную информацию. 4 Временный отрицательный ответ — команда отвергнута, но ошибка имеет временный характер, и команду можно повторить. I 5 Постоянный отрицательный ответ—команда отвергнута. Вот некоторые конкретные ответы, которые будут обрабатываться: Код Описание 125 Открыто соединение для данных—начинается передача. 150 Готовность к открытию соединения для данных. 200 Команда принята
TCP 163 продолжение таблицы 220 Обслуживание готово для нового пользователя. 227 Вход в пассивный режим. 230 Пользователь вошел в систему. 331 Имя пользователя принято — отправляйте пароль. Кодирование FTP-клиента После краткого обзора протокола FTP можно приступить к реализации клиента в .NET. Как упоминалось ранее, чтобы сделать этот класс как можно более понят- ным для разработчиков, при его создании реализуем класс FtpWebRequest, который наследует классу WebRequest и может использоваться аналогично. Он реализован как проект Class Library и состоит из пяти классов: □ FtpRequestCreator — Используется, когда регистрируется префикс “ftp" в WebRequest. , □ FtpWebRequest — Представляет запрос на копирование файла с FTP-сервера или на FTP-сервер. □ FtpWebResponse — Представляет ответ от сервера. □ FtpWebSt ream — Представляет поток между клиентом и сервером. □ FtpCIlent — Класс-утилита, который используется для соединения с сервером и выполнения команд FTP. Класс FtpRequestCreator В первую очередь нужно создать реализацию интерфейса IWebRequestCreate. В этом интерфейсе есть один метод С reate (), который вызывается статическим ме- тодом WebRequest. С reate (). В данном методе просто возвращается экземпляр класса FtpWebRequest: public FtpRequestCreatorО using System; using System.Net; namespace Wrox.. Networking.TCP.FtpUtil { “ ' < .. « public class FtpRequestCreator i IWebRequestCreate При желании создать объект FtpWebRequest нужно зарегистрировать префикс “ftp” и передать его в объект FtpRequestCreator, чтобы класс WebRequest знал, что он должен использовать этот класс для обработки любых Web-запросов, начинающих- ся в “ftp” (позднее узнаем, как это делается). —
164 Глава 5 Класс FtpWebRequest Далее можно определить сам класс FtpWebRequest. В нем будет пять членов дан- ных для сохранения имени пользователя и пароля, с которыми будет устанавли- ваться соединение, URI, с которым требуется соединиться, логического значения, указывающего, представлены ли данные в двоичном виде или в ASCH, и строки, ха- рактеризующей “метод” запроса. Последнее поле предназначено для хранения ко- манды, которую хотим выполнить на сервере; чтобы повысить доступность для пользователей, незнакомых с протоколом FTP, разрешим устанавливать в этом поле “GET” вместо “RETR” и “PUT” вместо “STOR”. Эти значения также представлены от- крытыми свойствами класса. Конструктор этого класса просто устанавливает в поле uri полученное значение Uri. Единственным методом этого класса является метод GetResponse(), который создает экземпляр нового объекта FtpWebResponse, передавая ему текущий FtpWebRequest как параметр: using System's* using System.Net; namespace Wrox Networking.TCP.FtpUtil public class FtpWebRequest : WebRequest { ' ' , ' ... ’ - v - X private string username = “anonymous”: internal string password « “someuser@soniemail.conr; private Uri uri; . . ’ private bool binaryMode я true, . private string method >= “GEPv * • ’»- ‘ r internal FtpWebReqoesttUrFurlT ’М.Ч - { i- '$3 this,uri = uri: public string Username { ’ '' - • get { return username: } set { username = value: } public string Password set { password » value: } } public bool BinaryMode { get { return binaryMode; } set { binaryMode = value; } } %' ,5 public override System.Uri RequestUri { get { return uri; } ' *•/!»."f4L? } .. - Л > ,. = ?>%'. •. • public override string Method .к;, { .. ...ft.:. get { return method; } set { method = value: } _ -
TCP 165 & .. } public override System.Net.WebResponse GetResponse() FtpWebResponse response = new FtpWebResponse(this); return response; ' -v < ) } Класс FtpWebResponse Теперь определим класс FtpWebResponse. В нем есть два закрытых члена-элемента данных: объект FtpWebRequest, с которым связан данный класс, и экземпляр класса FtpClient — в нем реализуется значительная часть кода, необходимого для взаимо- действия с FTP-сервером. Конструктор этого класса просто устанавливает поле за- прс са на переданный ему объект FtpWebRequest: using System; using System.IO; using System.Net; using System.Net.Sockets; ; - ,£• “ЙТ..Л i, « ... namespace Wrox.Networking.TCP-FtpUtil * public class FtpWebResponse д WebRespgnse private FtpWebRequest request; private FtpClient client;. internal FtpWebResponse(FtpWebRequest request) this.request * request; В методе GetResponseStream() как раз и выполняется соединение с сервером и ко- пирование данных. Этот метод сначала разделяет URI, с которым хотим соединить- ся, на составляющие части: во-первых, имя сервера и, во-вторых, путь и имя файла, который надо скопировать с сервера или сохранить на сервере. Далее создаем эк- земпляр класса FtpClient, передавая ему имя пользователя и пароль из объекта FtpWebRequest, и используем его для соединения с сервером. Затем выполняем коман- ду, представленную объектом FtpWebRequest. Это может быть команда GET/RETR для копирования файла с сервера или команда PUT/STOR для копирования файла на сер- вер. В любом случае извлекаем объект Networkstream, используемый для представле- ния потока между клиентом и сервером. Если указан какой-либо другой метод, мы порождаем исключение, чтобы констатировать, что метод не поддерживается. На- конец из объекта NetworkSt ream создаем новый объект FtpWebSt ream и возвращаем его вызывающей программе: public override System. 10. Stream GetResponseSt reamO { . " // Разбиваем URI, чтобы получить имя хоста и имя файла string hostname; z • string filename; GetUriComponents(request.Requestor!.ToString(h put hostname^ out filename);.. ’’ *'' // Соединяемся c FTP-сервером и получаем поток client » new FtpClient(request .Username, request.password);
% client,Connect(hostname); NetworkStream cFtaStieam » null; bi- switch (request ..Method) { case "GET”; case "RETR”: datastream = client.GetReadStream(filename, request,BiпалуMode); break; case "PUT”: case “STOR”: - da taSt ream « client,GetWriteSt reamffilename, *. w . r SJ roquest.BinaFyMode); drea'k; 5,^.. default:., >r - -.'•у/эдж’1/ throw new >eoException( “Method " ♦ request. Method:'+ .if» “ hot supported”); // Создан^ и возвращаем FtphebStream // (чтобы закрыть базовые объекты) 4 FtpWebStream ftpstream « new FtuWebStream(dataStreani, this);. returh ftpstream;: < Метод GetUriComponents() выполняет разбор URI в строковом формате и запол- няет два выходных параметра имени хоста и имени файла: private void GetUciCompdnents(string uri, out string hostname, ь out string fileName) // Проверяем, чго URI содержит не менее 7 сик одов, иначе - ошибка uri « uri.ToLowerO; i - 1 if (uri.Length < 7) , throw new UriFormatExceptionCThyalid URI"): // Проверяем, что URI начинается c “ftp://”, и удаляем эти символы if (uri„Substring(O, 6) £= “ftp://”.) - throw new NotSupporte^Exception( - к “Only F1P request* are supported”X else .> uri = uri.Substring^ uri.Length - 6)<. << // Разделяем ос¥автуься часть URI на имя хоста и имя файла stringO ufiPartj = uri.Spllt(new charf] { '/* ). 2): if (ufiParts.Lengt.i h* 2) throw new UrjFormatException(“Invalid URI”):; hostname = uriParts(6j; fileName 3 uriParts[1]: ) Д •. • 3 Наконец метод CloseQ просто вызывает Close() на объекте FtpClient: й public override void Close () * { clien. CloseO ;
TCP 167 Класс FtpWebStream Класс FtpWebSt ream наследует классу St ream. В нем есть два закрытых поля: объект FtpWebResponse, возвращающий его приложению-клиенту, и базовый объект NetworkSt ream, предназначенный для передачи данных между клиентом и FTP-серве- ром. И снова конструктор просто заполняет эти поля: using System; а; ' using System.IO; л. using System.Net.Sockets; namespace Wrox.Networking.TCP.FtpUtil { internal class FtpWebStream: Stream ( ' ... ... . private FtpWebResponse response; private Networkstream datastream; ... •.%/ • - z \ , public FtpWebStream(NetworkStream dataStream, FtpWebResponse response) this datastream * datastream; - ' this.response » response; ) •’* : , y- -• _ ' . . . 4.-^; Остальные методы и свойства просто передают вызовы базовому объекту Networkstream или (если запрошенная функциональность не поддерживается) по- рождают исключение NotSupportedException: public override void Close() t > response. Closed; base. Closet); .public override voio Flush() datastream.Flush(); ) public override int Read(byte[] buffer, int offset, int count) { return datastream.Read(buffer, offset, count); public override long Seekfiong offset, System.10.SeekOrigin origin) throw new NotSupportedException(“Seek not supported1’); public override void SetLength(long value) ж { throw new NotSupportedException(“SetLength not supported”); public override void Write(byte[) buffer., int offset, int count) datastream Write(buffer, offset: count); J -'-д public override bool CanRead
168 Глава 5 get { return datastream CanAead; } } ’ public override bool .GanSeek t get { return false; } } public overtide bool CanWrite get { return datastream.CanWrite; } ) public override leng Length get { throw new NotSupporieuException(“Length not supported"); F J p public override long Position get { throw new NotoupportedExceptionC’Position not supported”); } set { throw new NorSuppprtedException(“Position not supporteo”); } Класс FtpCIient В последнем классе проекта, FtpCIient, выполняется большая часть реальной ра- боты. К этому классу предполагается обращаться только из приложения, поэтому устанавливаем для него внутреннюю (internal) доступность и делаем его герметич- ным (sealed). В этом классе семь закрытых полей: □ bufferSize — константа, представляющая размер буфера, который будем ис- пользовать для чтения и записи с объектом Networkstream □ controlstream — объект Networkstream, используемый для отправки команд FTP-серверу и получения ответов □ datastream — объект Networkstream, используемый для обмена данными с FTP-сервером □ use г name — строка, представляющая имя пользователя, используемая для аутентификации на FTP-сервере □ .'assword — пароль, связанный с именем пользователя О client— объект TcpCIient, используемый для создания соединения по управле- нию с FTP-сервером О dataClient — объект TcpCIient, используемый с целью создания соединения цля данных с FTP-сервером using System; ч,« . ... . using System. Net; 1 using System. Net,. Sockets; "фг W;i using System 10; ' .>4/. / using System. Tert; namespact Wrox.Networking.TCP FtpUtil ’ < - internal sealed class FtpCIient
TCP 169 private const int buffersize • 65536;- private Networkstream controlStream; private Networkstream dataStream;-J private TcpCIient client; private string username; private string password; private TcpCIient dataClient » null; Конструктор принимает имя пользователя и пароль в параметрах и сохраняет их з членах-элементах данных: public FtpCIient(string username, string password) this, user name = username,; , this.password -- password; Метод Connect () выполняет начальное соединение с FTP-сервером. Он открыва- ет соединение с портом 21 (порт по умолчанию для управляющего соединения с FTP-сервером), используя класс TcpCIient, и получает для этого соединения класс Networkstream. Затем вызываем метод GetResponse(), извлекающий из потока код со- стояния и сообщение. Если все успешно, получаем ответ с кодом 220 и сообщением “service ready for new user”, после чего можем подключиться, передав имя пользова- теля и пароль. Любой другой код состояния означает неудачное соединение, поэто- му порождается исключение: public void Connect(string hostname) f 1 # • ’ C , | .? Я . ! // Устанавливаем закрытые поля, представляющие управляющее // TCP-соединение ;с сёрвербм йNetworkstream для взаимодействия // с сервером , J ? ~ - •- . > * client = new TcpCIient(hostname, 2F)i controlstream « client.GetStream!);. string responseMessage; if(GetResponse(out responseMessage)!= 220) i throw new WebException(responseMessage); : T . - г Logon!username, password); Метод GetResponse () используется для считывания ответа с FTP-сервера на управ- ляющем соединении. Он возвращает трехразрядный код состояния как целое число и заполняет выходной параметр ответным сообщением: public int GetResponse(out string responseMessage) < // Считываем отрет сервера, отбрасываем нули и возвращаем byte [] response = new bytefclient.ReceiveBufferSizeJ; controlstream.Read(response, 0, response.Length); responseMessage = Encoding.ASCII.GetString!response).Replace! return int. Parse( responseMessage. Substring!0,. 3)); Метод Logon() отправляет на сервер команду USER. Если серверу потребуется па- ро; », он ответит кодом 331, и в этом случае отправляется команда PASS с паролем пользователя. Иначе сервер должен ответить кодом 230, подтверждающим, что
170 Глава 5 пользователь подключился успешно. При любом другом ответе мы порождаем ис- ключение Unaut ho гizedAccessExcept ion. Заметим, что в FTP нет встроенных средств обеспечения безопасности, защищаю- щих пароли, поэтому пароли должны посылаться открытым текстом. По этой причине секретные пароли отправлять в сеть не следует. private void Logon(string, username, string password) // Отправляем команду USER: FTP Ссрвеп должен ответить сообщением // 331, запрашивая пароль пользователя. string respMessage; int resp » SendCommandC'USER *,+ username., out resoMessage); if Cresp I» 331 && resp != 230) :f throw new UnauthorizedAccessException( ”4;:-'' “Unable to logintd the? FTP server"); ЙП if (resp b= 230) / { л.. t: ' v. 1 ’4;: ‘ '^1 ’’ 7/ Ofпрёйлйёй команду PASS^FTP’- Сервер .доджей ответить рообШениём // 230, подтверждая вход в систему пользователя. - resp $ SendCommand(“₽ASS " + password, out respMessage); if (, esp ! « 230) , ’ throw new UnauthorizedAccessException( “FTP server can’t authenticate username’’-); ? } } Следующие два метода просто возвращают объект Networkstream для копирова- ния файла через соединение для данных, вызывая метод DownloadFileO или UploadFileO (рассмотрим их ниже): puolic Networkstream GerReadStream(string filename, bvOl ЫнагуМоое) I . return DownloadFile(filename, binaryMode)-; > public NetworkStream GetWriteStream(string filename, bool binaryMode) .. { • - return UploadFile(filename, binaryMode); } Метод DownloadFile() открывает соединение для данных с FTP-сервером и затем устанавливает binaryMode, указывая нужный формат данных: двоичный или ASCII. Затем отправляем команду RETR, сообщая, что хотим скопировать файл с сервера. На эту команду можно получить ответы 125 или 150, которые означают, что соедине- ние для данных или уже открыто, или готово к открытию. Получив другой ответ, мы порождаем исключение WebException. Иначе, если ответ положительный, возвраща- ем из нашего объекта dataClient класса TcpClient объект Networkstream: private NetworkStream bownloadFile(sTringsf ilename, cool binaryMoae) if (dataClient null) , dataClient = CreateDataSocket(); SetBinaryModeCbi/iaryModejj / string respMessage; int resp » SendCommandX"RETP '+• filename, out respMessage)';
TCP 171 if (resp != 150 && resp != 125) throw aew WebException(respMessage); dataStreanr = dataClient. GetStream(), return datastream; ,. J л _• -rrVC;... •• * 1 *"fe' / A •'• ?> Метод UploadFileO почти идентичен методу DownloadFile(), не считая того что вместо команды RETR отправляется команда ST0R: private NetworkStream UploadFile(string filename, bool binaryMode) ( - <- if (dataClient «= null) . dataClient » this. C reate Da taSocketO; // Устанавливаем режим: двоичный или ASCII - SetBinaryMode(bina ryMode); // Отправляем команду STOR для копирования файла на сервер string respMessage; int resp = SendCommand(“STOR " + filename, out respMessage); /ДМы должны получить ответ 150, означающий, что сервер // открывает соединение для данных , • if (resp != 150 && resp 1= 125) д * ’М throw new WebException("Cannot upload files to the server”); datastream = dataClient. GetStream(); return dataStream; } Метод SetBinaryModeO позволяет указать, хотим ли мы получить двоичные или текстовые (ASCII) данные. Это указание передаем FTP-серверу командой TYPE с па- раметром А для данных ASCII или I для изображений (двоичных данных). Если команда выполнена нормально, получаем код ответа 200: private void SetBinaryMode(bool binaryMode) int resp; ' ''' . ' :• je- st ring respMessage; if (binaryMode) resp = SendCommandC'TYPE Г1. out respMessage); else resp = SendCommand(“TYPE A*, out respMessage); if (resp .'= 200) v throw new WebException(respMessage); } Метод CreateDataSocket () — наиболее сложный из методов. Сначала для перехода в пассивный режим отправляем на сервер команду PASV. Сервер отвечает прибли- зительно таким сообщением: 227 Entering Passive Mode (192,168,0,1,177,147). Первые четыре значения в скобках представляют IP-адрес, а последние два—но- мер порта, на котором сервер слушает. С помощью этих значений получаем IP-адрес и номер порта, используемые сервером в соединениях для данных. Они возвраща- ются как 8-битные значения, поэтому вычисляем IP-адрес, сцепляя значения, разде- ляя их точками и формируя строку в обычном десятичном формате (например, “192.168.0.1"); номер порта получаем, сдвигая пятое значение влево на 8 битов
172 Глава 5 и прибавляя к нему шестое значение. Выполнив эти преобразования, создадим объ- ект TcpClientj на котором можно открыть соединение для данных с указанными IP-адресом и номером порта: private TcpCIient CreateDataSocket() // посылаем команду серверу - слушать на порте данных // (это не порт но умолчанию для данных) и < дать соединения string respMessage; int rasp * SendCommand(“PASV”, out respMessage); if (resp l = 227) throw new WebException(respMessage); ’ tt Ответ включает адрес хоста и номер порта // они разделены запятыми ft Создаем IP-адрес и номер порта intil parts - new int[6J; try irit index! = respMessage.IndexOff(‘); 1 int index2 - respMessage.IndexC). (*)’ ), string endPointOata т respMessage.Substring(index! + 1, index? - index! - !>; st ring[ 1 endPointPa rts = endPointcata. Split (‘,4); for (int i = 0; i < 6; 1++) Г parts[i] = iht.Barse(endPoirjtParts[i,I); } } - catch { throw new WebException(“Malformed PASV reply;. " + espMessage); } • - • string ipAddress :*- partsfO) + ,F.** + partsfl] + + partsJP] + : + parts[3], ',x ' • ' jcght port; = (parts. [4] < 8) + parts[5J, // Создаем сокет клиента TcpCIient dataClient = newTcpClientO; ; ff Соединяемся с портом данных сервера 4 try Л . IPEndPoint remoteED = new IPEndPoint(IPAddress.Parse(ipAddress), port>; oataCJ lent. Connect (nemo teEP); } catch(Exception) { ... . throw new WebException(“Can't connect to remote server"); return dataClient; Метод Close () просто считывает с сервера все неполученные ответы, заканчива- ет работу пользователя и очищает открытые ресурсы: public void CloseО < ' J' ... . ... ..... if (dataStre>«n nul.1) ' "* f' ‘
TCP 173 - dataStream. Closed; dataStream = null; } string respMessage; . GetResponse(out respMessage); LogoffQ; ' // Close the control TcpClient and NetworkStream cont rolSt ream. Closed; ; client Closed; .* } Метод Logoff () сначала в целях отладки отправляет серверу команду STAT, азатем команду QUIT. На команду STAT будет два ответа, поэтому нужно вызывать метод GetResponse(), чтобы считать второй ответ: л public void LogoffО { ,5 .. •• ' ' // Отправляем команду QUIT, чтобы отключиться от сервера String respMessage; 1 * SendCommandC'STAF", out respMessage); // Только тестирование < GetResponse(out respMessage); // У STAT 2 строки ответа! SendCommand(“QUIT”, out respMessage); } 3 последнем методе SendCommandO фактически выполняется запись команды в управляющий поток. Прежде чем отправить команду, добавляем за ней комбина- цию CRLF, чтобы не делать это каждый раз, когда вызывается метод. Отправив команду, мы вызываем метод GetResponse(), который считывает ответ сервера: internal int SendCommand(string command, out string respMessage) // Преобразуем строку команды (завершаемую символами CRLF) If в массив байтов и записываем ее в управляющий Поток byte[] request = Encoding.ASCII.GetBytes(command + “\r\n’’); „ controlstream.Writetrequest, 0, request.Length); return GetResponse(out respMessage); ) } На этом завершается код для библиотеки класса, и теперь напишем простое кон- сольное приложение, чтобы его протестировать. Консольное приложение-клиент В первую очередь в методе Main() нужно зарегистрировать префикс “ftp” в клас- се WebRequest. С этой целью вызываем статический метод WebRequest.Register- PrefixO, передаем ему префикс, связанный с новым расширением WebRequest, и реализацию интерфейса IWebRequestCreate, которая создаст экземпляр класса FtpWebRequest. После этого вызываем два метода, демонстрирующие копирование файлов на сервер и с сервера соответственно: using System; using System.IO; ? using System.Net;’ . ? using Wrox.Networking. TCP,, FtpUtil;
174 Глава 5 namespace TestClient : . f . - ' 4 -Лч1,' .» .... л .. . { class Classi { const int bufferSize = 65536; L • . static void Main(string[J args) // Регистрируем схему ftp. // В качестве альтернативы можно использовать файл config 5 WebRequest.RegisterPrefix(“ftp”. new FtpRequestCreator())^ UploadDemoO; DownloadDemo(); Ka Метод UploadDemo() создает новый экземпляр класса FtpWebRequest и устанавлива- ет в его свойствах имя пользователя и пароль, используемые при создании этого со- единения, формат данных для файла (здесь используется двоичный формат, поэтому устанавливаем в свойстве BinaryMode значение t rue) и метод, выполняемый на сервере. Поскольку надо скопировать файл на сервер, последнее значение может быть или “PUT”, или “STOR”. Далее вызываем метод GetResponseStream() соответствую- щего объекта FtpWebResponse, чтобы считать объект FtpWebStream, который сможем использовать для записи содержания файла в FTP-сервер. Чтобы это сделать, от- крываем объект FileStream, указывающий на файл, который хотим скопировать, и пересылаем его порциями по 65 536 байтов в FtpWebStream. Наконец закрываем объекты FtpWebStream (который закроет соответствующий ответ и отключится от сервера) и FileStream: // Копируем на сервер файл, использующий FtpWebRequest public static void UploadDemoO { FtpWebRequest req я (FtpWebRequest)WebRequest Create( “ftp://192.168.0.1/demofile. bmp” req.Username = "Administrator”; req.Password » "secret"; req.Method = “PUT"; // STOR или PUT req. BinaryMode = true; ....... Stream writeStream » req.GetResponseOGetResponseStreamO; FileStream fs = new FileStream(@"c:\temp\cool. bmp'", FileMode.Open); byte[] buffer = new byte[bufferSize]; int read; while ((read « fs.Read(buffer, Q, bufferSize)) >0) writeSt ream.Write(buffer, 0, bufferSize);' writeStream.Close() ; ~ fs.CloseO; ' Sk <'"'•* Л 1 7 J .1. -I . iill.iii I 1 ..... К gjRI mJ— .. > .• ;v Обратите внимание, что для копирования файлов на сервер потребуются соответствующие разрешения на FTP-сервере. По умолчанию копирование файлов на сервер не разрешено. Г ' ' : ' ай lit'" , ' , , ф :-гх ’I..-. ,;- / I а " 14 'ft_V. Метод DownloadDemoO похож на UploadDemoO, но способ его использования зави- сит оттого, копируются ли двоичные данные или текст. Если это двоичные данные,
TCP 175 копируем данные файла в объект FileStream порциями по 65 536 байтов, как в UploadDemo(). Если это текстовый файл, можно воспользоваться гораздо более про- стым объектом StreamReader: // Копируем файл с сервера, используя FtpWebRequest ! j \ public static void DownloadDemoO { - • FtpWebRequest req « (FtpWebReque$t)WebRequest.Create( . K^,‘ftp://192<l68iQ.Vsample,bmp,,>; ^'b // no умолчанию: A req.Username » "anonymous”; : req.Password - "someuser@somemail.com”; req.BinaryMode = true; req>Method * “GET”; */ FtpWebResponse resp » (FtpWebResponse)req.GetResponseO; Stream stream = resp.GetResponseStreamQ. - '« 15 ’ it. « * . // Считываем двоичный файл FileStream fs = new FileStrearn(@"c:\temp\sample.bmp", ... FileMode.Create); bytefl buffer = new byte[bufferSize]; > ‘ int count; do { Array.Clear(buffer, 0, bufferSize); 4 count = stream.Read(buffer, 0, bufferSize); fs.Write(buffer, 0, count); } while (count > 0У; Л ,4 -.4 ' ’’ I 4 . J • • ... stream. CloseO; & : Ь fs.CloseO; ;'v'" <- Г А считываем текстовый файл ’ •; .StreamReader reader = newStreamReader(stceam); string line; * >' .... v while ((line » reader.ReadlineO) != null) < " Console. Writeline( line J; У reader.CloseO; »/ Проект завершен! Это не самая трудоемкая реализация, в ней учтены далеко не все возможные команды, но она должна дать ясное представление о том, что требуется для реализации FTP-клиента, и служит примером реализации протокола прикладного уровня, немного более сложного, чем SMTP. Этот проект также показывает, как можно объединить классы с существующей средой WebRequest, чтобы обращения к ним были понятны. Класс TcpListener Обычно приложение стороны сервера начинает работу, связываясь с локальной конечной точкой и ожидая входящие запросы от клиентов. Как только клиент дос- тиг порта, приложение активизируется, принимает запрос и создает канал, пред- назначенный для взаимодействия с этим клиентом. На основном потоке приложение продолжает ожидать другие входящие запросы от клиентов. Класс
1?6 Глава 5 TcpListener делает именно это — он слушает запросы клиентов, принимает запрос и создает новый экземпляр класса Socket или класса TcpClient, которые можно ис- пользовать для взаимодействия с клиентом. Каки TcpClient, класс TcpListener также инкапсулирует закрытый объект Socket, m_Se rve rSocket, доступный только для про- изводных классов. В следующих таблицах показаны важные свойства и методы: • Открытые свойства ' Имя Тип Описание LocalEndpoint IPEndpoint Это свойство возвращает объект IPEndpoint, который содержит информацию о локальном сетевом интерфейсе и номере порта, используемую для ожидания входящих запросов от клиентов. Открытые методы Имя Описание AcceptSocketO Дает согласие на ожидающий запрос соединения и возвращает объект Socket, используемый для взаимодействия с клиентом AcceptTcpClientO Дает согласие на ожидающий запрос соединения и возвращает объект TcpClient, используемый для взаимодействия с клиентом. PendingO Указывает, есть ли ожидающие запросы соединения. Start() . Принуждает TcpListener начать слушать запросы соединения. Stop() Закрывает слушающий объект. Защищенные свойства Имя Тип Описание Active bool Указывает, слушает ли в настоящий момент TcpListener запросы соединения. Server Socket Возвращает базовый объект Socket, используемый объектом TcpListener, чтобы слушать запросы соединения. Создание экземпляра класса TcpUstener Существуют три перегруженных конструктора TcpListener: public TcpListener(int port); , public TcpListener(IPEndPoint endPoint); public TcpListener(IPAddress ipAddr, int port);
TCP 177 Первый конструктор просто указывает, какой порт используется, чтобы слу- шать запросы. В этом случае IP-адрес равен IPAddress. Any, т. е. сервер принимает действия клиентов на всех сетевых интерфейсах. Это значение эквивалентно IP-ад- ресу 0.0.0.0: AMnt рогС= 11000; ,Л: , "У TcpListener newListener ? new TcpListener(port); > • Второй конструктор принимает объект IPEndPoint, определяющий IP-адрес и порт, на котором хотим слушать: IPAddressIpAddr =IPAddress.Parse(“127.0.0 1); IPEndPoint endPoint i new IPEndPoint(ipAddr. 11000)jr v C TcpListener newListener:? new TcpListener(endPoint); * ‘ 4 ' Последний перегруженный конструктор принимает объект IP Address и номер порта: IPAddress ipAddr * IPAddress-.Parse(“l27v0.0.r); ant port-t $000; ^ч.- TcpListener: newListener? new TcpListener(ipAddr, port); Прослушивание клиентов На следующем шаге после создания сокета начинаем слушать запросы клиентов. В классе TcpListener есть метод Start(), выполняющий такую последовательность. Сначала он связывает сокет, используя IP-адрес и порт, переданные в параметрах конструктору TcpListener. Затем, вызвав метод Listen() базового объекта Socket, он начинает слушать запросы соединений от клиентов: TcpListener newListener « new TcpListener(ipAddr, port)? newListener.Start(); Уже приступив к прослушиванию сокета, можно вызывать метод PendingO, что- бы проверять, нет ли ожидающих запросов соединения в очереди. Этот метод позволяет проверять наличие ожидающих клиентов до вызова метода Accept, кото- рый будет блокировать выполняющийся поток: -if (newListener. PendingO) • ' ’f; 4? Console.Write.Line("There are connections pending in queue’-); Прием соединений от клиентов Типичная серверная программа оперирует двумя сокетами: один используется классом TcpListener, а второй — для взаимодействия с отдельным клиентом. Чтобы дать согласие на любой запрос, ожидающий в настоящий момент в очереди, можно воспользоваться методом AcceptSocketO или более простым методом AcceptTcpCli- ent (). Эти методы возвращают соответственно объекты Socket или TcpClient и дают согласие на запросы клиентов. Socket sACcepted = newListener.AcceptSocketO; TcpClient sAccepted = newListener.AcceptTcpClientO; l Отправка и получение сообщений В зависимости от типа сокета, созданного при установлении соединения, реаль- ный обмен данными между клиентом и сокетом сервера выполняется методами Send() и ReceiveO объекта Socket или с помощью чтения-записи объекта Net-
178 Глава 5 workstream. Эта тема была подробно обсуждена в разделе, посвященном классу TcpClient, и поэтому здесь соответствующий код опускается. / Остандвка сервера После завершения взаимодействия с клиентом нужно выполнить последний шаг — остановить слушающий сокет. Для этого вызывается метод Stop() объекта TcpListener: newListener. StopO; Многопоточное приложение клиент-сервер В этом разделе построим простой многопоточный эхо-сервер, который, исполь- зуя потоки, сможет возвращать нескольким клиентам получаемые от них сообще- ния. Прежде чем перейти к самому коду сервера, представим краткое введение в многопоточность на небольшом примере, посвященном созданию и выполнению потока. Поскольку эта тема не связана непосредственно с сетевым программирова- нием, не станем пытаться освятить многопоточное программирование в деталях и предположим, что читатель знаком с основными понятиями организации обра- ботки потоков. Поддержку многопоточности в .NET обеспечивает пространство имен Sys- tem. Threading. Класс Thread из этого пространства имен представляет отдельный поток. Для запуска альтернативного выполнения кода потоку нужна точка входа. Эта точка входа определяется методом, с которого начнется выполнение потока. Естественно, что этот метод представлен делегатом, а именно — Threadstart. Следовательно, прежде чем создавать экземпляр класса Thread, нужно построить экземпляр делегата Threadstart, указав в конструкторе имя метода. Указав метод с помощью делегата в конструкторе Th read, надо следующим шагом сообщить операционной системе об изменении в состоянии потока. Метод Start() класса Thread уведомляет операционную систему, что поток изменил состояние и начал выполняться: // Threading.cs < using System; I using System Threading, public class MultiThread "Ф- { . .?' - - ’ public static void runThreadO Console.WriteLine(“Thread is running”!; public static void Main(string [] arg) // Threadstart задает функцию-делегат Threadstart threadMethod = new ThreadStart(runThread); // Создаем экземпляр потока с делегатом Threadstart Thread newThread sjiew Thread(threadMethod); Console.WriteLine(“Starting Thread”); '.утЛ - C* . • -r .'Ч-Л-Л // Открываем поток, который вызывает метод гunThread newTh read.Start(); ‘даЧ: . - У • в отдельном потоке
TCP 479 Это все, что нужно знать об использовании потоков в элементарном многопо- точном сервере, хотя для создания более сложного серверного приложения потре- буется более интенсивно использовать класс Thread, поддерживать такие приемы синхронизации, как блокирование, и более развитые средства управления потока- ми, например пулы потоков. Клиентом будет простое приложение, почти такое же, как мы создали в предыду- щем разделе для класса TcpCIient, поэтому не нужно рассматривать его слишком пристально: using System: using System.Net; using System.10; - using' System/Net.Sockets; using System.Text; public class EchoClient { const int ECHO_PQRT 8080; public static void Main (st ring [J arg) *’>•.../ . . - Console? Write(’*Your UserName:");< - :string userName* Console.ReadLine(); S Console.WriteLineC—Logged In—>’*) • // Создаем соединение с ChatServer TcpCIient eClient = new TcpCiient("i27.0.0.v, ECHO.PORT); // Создаем классы потоков StreamReader readerstream * new StreamReader(eClient.GetStreamO); Networkstream writerstream = eClient.GetStream(); string dataToSend; dataToSend = userName; dataToSend *= “\r\n”; :.// Отправляем имя пользователя на сервер byte[] data - Encoding.ASCII.GetBytes(dataToSend) ; writerstream.Write(data, O', data.Length); while(true) Console.Write(userName + ж /7 Считываем строку с сервера dataToSend = Console.ReadLine(); dataToSend += “\r\n"; data •« Encoding.ASCII.GetBytes(dataToSend); writerstream.Write(data, 0, data.Length); 7/ Если отправлена команда QUIT, выйти из приложения if (dataToSend.IndexOf(“QUIT”) >-1) 7 d* break;,
180 Глава 5 string returnData; •: fl Получить отве’. от сервера return Data = readerstream ReadLineO; Console Writeltne(’Server: ” + returnData); } // Закрыв TcpClient eClient Clo$e(); ) catch(Exception exp) { ’ Console.WriteLine(‘'Exception: ” + exp); ) Приложение-сервер, очевидно, претерпело изменения по сравнению с тем, что было описано в предыдущей главе, — в нем используется несколько потоков для од- новременного обслуживания нескольких клиентов. На следующей схеме показана последовательность выполнения серверного приложения: Как обычно, в первую очередь создаем экземпляр класса TcpListene г и задаем но- мера порта, к которому привязываем сокет. Затем этот сокет начинает слушать и принимать запросы клиентов. Изменения в коде появляются, когда слушающий со- кет принимает запрос клиента. В модели сервера с единственным пользователем, где в каждый промежуток времени обслуживается один клиент, для непосредствен- ного взаимодействия с клиентом создается объект Stream. После завершения рабо-
TCP 181 ты с существующим клиентом тот сервер мог начать слушать нового клиента. Все задачи, реализованные в том приложении, выполнялись в одном потоке. В многопо- точном приложении слушающий сокет, дав согласие на соединение с сокетом кли- ента, запускает новый поток, предназначенный для ведения обмена с клиентом. Основной поток сервера продолжает слушать следующие запросы. Выполняемый во вторичном потоке метод реализован в другом классе — Client- Handler. Вводим переменную-член, чтобы передавать данные между основным и вторичным потоком. Когда слушающий сокет принимает запрос и создает TcpCli- ent, строим новый экземпляр класса ClientHandler и назначаем объект TcpClient его открытому полю. Затем выполняем метод RunClientO в новом потоке. Поскольку этот метод принадлежит классу ClientHandler, он может обращаться к объекту TcpClient, который установлен из основного потока. • Метод RunClientO класса ClientHandler отвечает за весь обмен с одним клиентом. Он выполняет процедуру создания потоков, чтения и записи сообщений в сокет. Далее приводится полный листинг кода для серверного приложения: // MEchoServer.cs using System; , ' / ;-f. •. Л2 x\ ? using System. Net; ?.. using. System. Net .Sockets;.. f . using System. 10; - using System.Threading; using System.Collections; using System.Text;: : «~ fa fl'U public class ClientHandler i " ' s public TcpClient clientsocket; public void RunClientO // Создаем классы потоков StreamReader readerstream = new StreamReader(clientSocket.GetStreamO); Networkstream writerstream * clientsocket. GetStreamO; string returnData = readerstream. ReadLineO; string userName - returnData; Console.WriteLine(“Weicome " + userName + “ to the Server*’); while (true) > I i . . returnData « readerstream.ReadilneO; if (returnData IndexOf(“QUIF) > -1) Console WriteLineC’Bye Bye ” + userName); <* break; } * Console.WriteLine(userName + " : ” + returnData); returnData += “\r\n’’; byte[] dataWritC - Encoding.ASCII.GetBytes(returnData); ДwriterStream.Write(dataWrite, 0, dataWrite.Length) ; X S. ’ ’ ' . - ** *<йг*Г’* , , “ * ’ • }!. ‘ ' * к < ' clientsocket.Closed; 1
182 Глава 5 Л::. ' public class EchoServer л. X, : Х-\, { «А ' . ., - •« '' , 'йХХ Z const int ECHOJPORT е 8080;5у. v \ public static int nClients = 0; public static void MaipUtring [] arg'» t •<’ЬЛ < •> ?$ '••»•.’>.• A .5 ^-V* ’ • * -:A ' try У\ Vf< - р^Л/\‘у, t ,-y' * •> *'• • - . ’• // Связываем сервер с локальным портом TcpListener cllenttistener = new Tcplistener(ECHO_PORT); // Начинаем слушать clientLisiener.Start(•); Console WfiteLine(“Waiting for connections .ЛХ while (true) { // Даем согласие на соединение TcpClient client =• clientListener.AcceptTcpClientO; ClientHanoler cHanoler = new CllentHandlerOi; // Передаем значение объекту ClientHandlef cHandler.clientsocket = client; * // Создаем hobw. поток для клиента Thread clientThread « new Thread(new Threadstart (cHandler.RunCllent)); cUentThr3ad, StartO; clientListener.StopO;; - } ;atch(Exception expJ ( Console.WriteLine(“Exception* ” + exp:); > ) } Это приложение можно протестировать, если запустить сначала сервер, а затем несколько экземпляров клиента и войти в них с разными именами. Хотя это и про- стейший сервер, созданный исключительно ради демонстрации, его можно исполь- зовать как каркас для создания более сложных серверных приложений. .NET Remoting Обычно операционные системы обеспечивают каждому приложению некото- рый уровень защиты от воздействия других приложений. Это средство необходимо, чтобы сградить одно приложение от последствий ненормального выполнения дру- гого приложения. Система Microsoft Windows придерживается этого подхода, реализуя процессы для каждого приложения. Каждое приложение загружается в изолированное пространство процесса. Код и память, принадлежащие одному процессу, недоступны для кода, выполняемого в другом процессе, хотя такое обра-
TCP 183 щение все же возможно через широко известные методы, управляемые операцион- ной системой. В среде .NET Framework компания Microsoft ввела понятие доменов приложений (application domains, или AppDomains). Домены приложений Домены приложений представляют собой элементы, которые используются средой .NET Framework, чтобы изолировать два разных приложения. В одном про- цессе могут выполняться несколько доменов приложений. Это основанное на Арр- Doi^ain изолирование предоставляет несколько глобальных преимуществ, особенно для приложений на стороне сервера. Существование нескольких разных доменов приложений в одном процессе означает, что, если функционирование ка- кой-либо части кода в одном домене приложения по каким-то причинам нарушено, это не нанесет вреда другим AppDomain в том же самом процессе. Кроме того, мож- но остановить часть кода, включенную в какой-либо домен приложения. Однако важнее всего то, что код, выполняемый в одном домене приложения, не может обра- щаться к коду в другом домене. Процесс, используемый для обеспечения взаимо- действия между разными объектами в разных доменах приложений, называется .NET Remoting. Среда Microsoft .NET Remoting может использоваться, чтобы сделать возмож- ным взаимодействие между двумя разными приложениями. Эти приложения могут выполняться на одном компьютере, в одной локальной сети, в Интернете или в ка- кой-либо географической области, связанной некоторым протоколом. рдно из преимуществ .NET Remoting обусловлено тем, что в отличие от фир- менных протоколов, применяемых в Microsoft DCOM и Java RMI, среда Remoting построена на таких принятых промышленных стандартах, как Simple Object Access Protocol (SOAP), HTTP и TCP. По этой причине самые разные приложения, работа- ющие в Интернете, могут взаимодействовать точно так же, как если бы они устанав- ливали соединение в одной частной сети. Хотя Remoting может использовать протоколы SOAP и HTTP, это необязатель- но. Стоит хорошенько подумать, прежде чем использовать Remoting с SOAP, по- скольку будут потеряны все преимущества, которые имеет среда Remoting по сравнению с Web-сервисами ASP.NET. Если вы используете не Web-сервисы, a Re- moting, эффективность, вероятно, является самым значимым фактором, и, чтобы получить наилучшую производительность, выберите для канала TCP двоичное ко- дирование. Толное и глубокое описание среды Microsoft .NET Remoting выходит за рамки этой книги. Обсудим самые важные понятия при построении приложения Remo- ting на примере, который покажет, как можно создать собственное приложение для среды Microsoft .NET Remoting Framework. Как работает Remoting Чтобы можно было пользоваться средой Remoting, сначала нужно загрузить при- ложение-хост и зарегистрировать канал и порт для ожидания запросов соединения. Клиент должен зарегистрироваться на том же самом канале. В следующем разделе обсудим каналы более детально, но в данный момент канал можно считать транспорт- ной средой между сервером и клиентом. Для отправки данных от одного объекта к другому каналы пользуются таким сетевыми протоколами, как TCP или HTTP. Пос- ле того как клиент зарегистрировал канал, он создает новый экземпляр удаленного клсССа. Если клиенту удается создать экземпляр удаленного объекта, он получает ссылку на серверный объект и через него может вызывать методы на удаленном объ- екте, словно он является частью клиентского процесса. Для реализации этой возмож-
184 Глава 5 ности на удаленной системе используется прокси-объект. Когда клиент создает экземпляр удаленного объекта, среда Remoting перехватывает этот вызов и создает прокси-объект с таким же открытым интерфейсом, как у реального объекта, и возвра- щает этот прокси-объект клиенту. Затем клиент вызывает методы на этом прок- си-объекте. Вызовы перехватываются средой Remoting и направляются к серверному процессу. Серверный процесс создает экземпляр объекта, вызывает метод и отправ- ляет возвращаемое значение клиентскому приложению через прокси-клиент: Каналы Каналы используются для транспортировки сообщений к удаленным объектам и обратно. Когда клиент вызывает метод на серверном объекте, параметры и другая информация, относящиеся к методу, упаковываются в объект сообщения и достав- ляются по каналу к удаленному объекту. Клиент может выбрать любой тип канала, зарегистрированный на сервере для использования транспортной средой. Пбэтому разработчики могут выбирать канал, наилучшим образом удовлетворяющий их по- требности. Хотя имеется пара встроенных типов транспортного канала, их можно расширить и даже создать абсолютно новый тип канала для использования в специ- ализированных средах и сценариях. Способы возвращения с сервера результатов аналогичны для разных типов каналов. Канальные приемники Каналы направляют каждое сообщение через последовательность канальных приемников (channel sinks). Каждый приемник изменяет или фильтрует сообще- ние, а затем передает его следующему приемнику в цепочке, цриложению-получате- лю или самому каналу. Сообщения, отправленные по каналу, могут обрабатываться любым числом приемников, в их числе могут быть, например, приемник безопас- ности (для шифрования сообщения) и приемник регистрации (для отслеживания удаленного процесса). На стороне клиента первый приемник—это сток форматирования, он передает сообщение любому специализированному приемнику, реализованному в цепи при- емников. Наконец сообщение достигает транспортного приемника, который его за- писывает в транспортный канал. На серверной стороне весь процесс выполняется в обратном порядке. Тран- спортиый приемник получает сообщение из канала и направляет его цепочке канальных приемников. Замыкает цепочку приемник форматирования, который направляет сообщение в инфраструктуру Remoting, и оно передается принимающе- му приложению:
TCP 185 i Средой .NET Framework обеспечены два типа канала: □ TcpChannel □ HttpChannel Канал TcpChannel обсуждается в следующем разделе, а в главе 8 содержится раз- дел, посвященный каналу HttpChannel. Средства форматирования Приемники форматирования используются для кодирования и декодирования сообщений перед их отправкой в канал. В среде .NET Remoting обеспечены два средства форматирования: 1. Двоичный форматтер. Если для сообщений используется двоичный формат, то и клиенту, и серверу требуется, чтобы .NET Framework обрабатывала сообщения. Типичная система поддерживает свое состояние, когда передача выполняется с применением двоичного форматтера. Поскольку двоичный форматтер зависит от .NET Framework, он может хранить информацию типичной системы через сериализацию и десериализацию объекта. У двоичного форматтера производительность лучше, чем у SOAP-форматтера, потому что меньшие объемы данных пересылаются через сеть и меньше времени требуется на сериализацию и десериализацию. 2. SOAP-форматтер, базирующийся на XML. SOAP-форматтер • придерживается спецификаций SOAP, подготовленных W3C (http://www.w3.orgnR/2002/WD-soap12-part1-20020626/). Благодаря этому он допускает возможность взаимодействия с другими клиентами или серверами. Поскольку SOAP-форматтер может включать разные реализации из разных языков, для него невозможно хранить информацию типичной системы. Поскольку SOAP-форматтер в большей степени ориентирован на расширение возможностей взаимодействия, чем на эффективность, то он обычно дает худшую производительность, чем двоичный форматтер.
186 Глава 5 Транспортный канал TCP Транспортный канал TCP по умолчанию использует двоичный форматтер для сериализации всех сообщений в двоичный поток и переносит их по назначению с помощью протокола TCP. Среда .NET Framework обеспечивает пространство имен System. Runtime. Remoting. Channels. Tcp, которое должно использоваться в прило- жениях, обращающихся к транспортному каналу TCP. В этом пространстве имен со- держатся три класса: Класс Описание TcpChannel Обеспечивает реализацию канала отправитель-получатель с использованием передачи сообщений протокола TCP TcpClientChannel Обеспечивает реализацию канала клиента, использующего протокол TCP для передачи сообщений. TcpServerChannel Обеспечивает реализацию канала сервера, использующего протокол TCP для передачи сообщений. В следующем примере показано, как написать простое приложение для среды Remoting. Клиент передает строку удаленному объекту, а тот отправляет обратно клиенту ту же строку. Процесс построения приложения для среды Remoting состоит из трех шагов: 1. Создание удаленного объекта 2. Создание приложения-хоста для удаленного объекта 3. Создание приложения-клиента Создание удаленного объекта Удаленный объект — это экземпляр обычного класса. В нем требуется сделать единственное дополнение — он должен наследовать классу MarshalByRefObject. Все разработанные классы можно преобразовать в удаленные классы, добавив наследо- вание от класса Ma rshalByRef Ob j ect. В этом классе может быть любое число методов и свойств. Здесь определим простой класс, с помощью которого можно создать экзем- пляр удаленного объекта. Прежде чем начинать любое приложение для среды Remoting, нужно включить все необходимые пространства имен. Для удаленного приложения, базирующегося на транспорте TCP, используются следующие пространства имен: // ServerOtjjv’cs-'l^M/. 'W«V f г using System.Runtime. Remoting; 4 ' v | using System Runtime Remoting Channels; ' using System.Runtime.Remoting.Channels.Tcp; Л < 41. Далее нужно определить класс для удаленного объекта: // Этот класс порожден от класса MarshalByRefObject public class ServerObj : MarshalByRefObject public string ServerMethod(string argument) - । Z . У” ‘ Console. WriteLine(argument)r; Г ’\ '' Ши
TCP 187 ЙйУ ; r return argument; ,£ jg. rlw W Этот класс надо построить как DLL, поэтому для компиляции удаленного объек- та используем следующую команду: esc /targetzlibrary /out:ServerObj.dll ServerObj.es Создание приложения-хоста Подготовив удаленный объект, создадим приложение, которое вмещает наш удаленный объект на сервере. Здесь создадим простое консольное приложение (хотя на практике вам может потребоваться рассмотреть идею использования Windows-сервисов как лучшую альтернативу). Сначала нужно, как и в приведенном выше коде, включить все директивы using. Далее создадим и зарегистрируем TcpChannel. (Но прежде нужно закрыть сервер, со- зданный в предыдущем разделе, поскольку Remoting используется на том же порту.) TcpChannel tcpChannel = new TcpChannel(8080); . Конструктор TcpChannel используется для указания номера порта, предназначен- ного для ожидания соединений клиентов. Далее мы регистрируем канал TCP: Channelservices.Registerchannel(tcpChannel); Класс Channelservices предоставляет статические методы для регистрации и де- регистрации каналов, создания цепочки канальных стоков, диспетчеризации сооб- щений в цепочке приемников и обнаружения URL. Ко всем зарегистрированным каналам можно обращаться через свойство RegisteredChannels, возвращающее мас- сив объектов IChannel. Самым важным в этом классе является метод RegisterChan- nel(). Он выполняет регистрацию канала с канальными сервисами, принимает единственный параметр — созданный нами раньше объект канала: public static void Registerchannel(IChannel channelobject); Здесь важно заметить, что нельзя зарегистрировать в домене приложения два каналас одним и тем же именем. По умолчанию каналы HttpChannel и TcpChannel име- ют имена “http” и “tep” соответственно. Для регистрации двух каналов надо изме- нить имя соответствующего канала в свойстве ChannelName объекта канала. После создания и регистрации канала для регистрации класса в среде Remoting вызывается метод RegisterWellKnownServiceType() класса RemotingConfiguration: RemotingConfigurationRegisterWellKnownServiceType( “ - t . .. % typeof(RemoteSample.Se rve rObj), "EchoMessage”, .„sC WellKnownObjectMode. SingleCall); •. - Класс RemotingConfiguration предоставляет статические методы для конфигури- рования опций среды Remoting. Метод RegisterWellKnownServiceType() принимает три параметра, которые указы- вают тип удаленного класса, строку, идентифицирующую удаленный объект (т. е. его URI), и режим активизации объекта. Для удаленных объектов есть два типа активизации: □ Серверная активизация. Объекты, активизируемые сервером, создаются сервером только по мере необходимости, например, когда клиент вызывает первый метод на этом сервере. Для активизируемых сервером объектов есть
188 Глава 5 два режима активизации — Singleton и SingleCall. У объектов Singleton есть только один экземпляр независимо от того, сколько клиентов существует для этого объекта. Когда для объекта задан режим SingleCall, новый экземпляр объекта создается каждый раз при вызове метода клиентом. □ Клиентская активизация. Объекты, активизируемые клиентами, создаются на сервере, когда клиент вызывает new или Activator. Createlnstance(). В примере кода мы создали объект, активизируемый сервером в режиме SingleCall. Наконец ждем, когда клиент соединится с сервером, блокируя входной поток методом Console. Headline(): Console. ReadLineO ; Далее для справки приводится полный исходный код: /7 ServeMpp.cs - гр using System; М ' 'Л/ “• using System.Runtime.Remoting; using System.Runtime. Remoting.Channels; using System.Runtime.Remoting.Channels.Tcp; namespace RemoteSample { public class ServerApp public static void Main(string [] arg) TcpChannel tcpChannel « new TcpChannel! 8080); Channelservices.Registerchannel(tcpChannel); л RemotingConfiguration.RegisterwellKnownServiceType( typeof(RemoteSampls.Server0bj),* 4 -'л "EchoMessage”, " VУ 7 / WellKnownObiectMqde-SingleCail);. : ' , Ж Console. WriteLine( "Hit <enter> to continue..;- Console. ReadllneOT * r‘ ’ J • 'b . -Ж+- } Создание приложения-клиента Последний шаг в реализации примера для работы в среде Remoting заключается в создании приложения-клиента. Как и приложение-сервер, клиент также должен зарегистрировать канал TCP с канальными сервисами: TcpChannel tcpChannel « new TcpChannel (); Channelservices.Registerchannel(tcpChannel); Внимательный читатель мог заметить, что IP-адрес и номер порта сервера опу- щены. Обычно клиенту для соединения с сервером требуются серверные IP-адрес и номер порта. Здесь не указывается ни то, ни другое. Чуть позднее узнаем, как среда Remoting обходится без этих данных. Следующее действие имеет для клиента наибольшее значение — на сервере соз- дается объект, и ссылка на него возвращается клиенту:
TCP 189 ServerObj obj = (ServerObj)Activater.GetObject(typeof(RemoteSample ServerObj), - 'w • ' ’ ‘‘tcp://localhost:8080/EchoMessage”); Класс Activator предоставляет методы для локального или удаленного создания типов объектов и получения ссылок на существующие удаленные объекты. Метод GetOb ject () создает прокси-объект для выполняемого в данный момент удаленного хорошо известного объекта, активизируемого сервером. Тип объекта и URL пере- даются как параметры. Обратим внимание на URL, который используется для опре- деления протокола, номера порта и IP-адреса удаленного объекта — всего того, что требуется для соединения с удаленным сервером и что было пропущено в конструк- торе TcpChannel. Здесь испытываем пример на одном PC, поэтому указываем имя localhost, но его можно заменить IP-адресом любого сервера, на котором хотите разместить удален- ный объект. После создания прокси-объекта и получения его средствами ссылки на удален- ный объект можно вызывать любые методы, определенные в удаленном объекте: ч obj.ServerMethod(“Wrox Remoting Sample*)! ? “1 Далее приведен полный код клиента: using System; * ч. . > . using Sy stem. Run time. Remoting; > using System.Runtime. Remoting.Channels; using System,Runtime.Remoting.Channels.Tcp; namespace RemoteSample ; < { " , -i • public class ClientApp I i - public static void Main(string[] arg) 4 , { Л - •• - V TcpChannel tcpChannel = new TcpChannelO; Channelservices. RegisterChannel(tcpChannel); ServerObj obj = (ServerObj)Activator GetObject( typeof(RemoteSample.ServerObj), utcp://localhost:8080/EchoMessage”); \ Console.Writeline(obj .ServerMethod(“Wrox Remoting Sample'*)); При компиляции клиентского и серверного приложений нужно включить ссыл- ку на DLL серверного объекта, иначе получим сообщение об ошибке: “The type or namespace name ServerObj does not exist in the class” (Имя типа или пространства имен ServerObj отсутствует в классе). Сервер компилируется так: esc /r:ServerObj.dll ServerApp.es А клиентское приложение такой командой: esc /r:ServerObj.dll ClientApp.es После компиляции всех файлов надо сначала запустить серверное приложение, чтобы подготовиться к получению клиентских запросов.
190 Глава 5 Создание удаленного объекта, активизируемого клиентом Приведенный выше пример демонстрирует удаленное создание экземпляра объекта, активизируемого сервером. Хотя для объекта, активизируемого клиентом, удаленный объект остается прежним, в серверное и клиентское приложения необ- ходимо внести некоторые изменения. В серверном приложении вместо метода RegisterWellknownServiceTypeO вызываем RegisterActivatedServiceTypeQ- Этот метод принимает единственный параметр, представляющий тип удаленного объекта. В этом случае URI назначается с использованием свойства ApplicationName класса RemotingConfiguration: using System; using System. Runtime.Remoting; using System.Runtime Remoting.Channels; using System.Runtime.Remoting.Channels.Tcp; namespace RemoteSample . .* public class ServerAop л-—, J J { - * .. Ж "* ° public static.M)id Mainfsttlng [] arg) J / .. _ { л . ’ьТ TcpChannel tcpChannel = new TcpChannel(8080); ChannelServices.RegisterChannel(tcpChannel) ; RemotingConfiguration.ApplicationName « “EchoMessage"; RemotingConfiguration.RegisterActivatedServiceTypeQ typeof(RemoteSample.Serve rObj)); Console.WriteLine(“Hit <enter> to continue..J’); Console. ReadLineO; •• } На стороне клиента в нашем распоряжении имеются два метода обращения к удаленным объектам: Activator.CreatelnstanceO и RemotingConfiguration.Regis- terActivatedClientType(). Для создания экземпляра удаленного объекта с использо- ванием ключевого слова new надо сначала зарегистрировать тип объекта на клиенте, вызвав метод RegisterActivatedClientType(). Вызов метода Createlnstance() дает новый экземпляр серверного объекта и требует передать ему в параметре URL удаленного приложения. Для передачи параметров методу Сreatelnstance() исполь- зуется класс UrlAttribute. Метод CreatelnstanceO возвращает объект ObjectHandle, служащий для удаленного объекта оболочкой, и, чтобы получить сам удаленный объект, нужно вызвать UnWrap() объекта ObjectHandle: using System; using System.Runtime.Remoting: using System. Runtime. Remoting.Channels; using System.Runtime Remoting.Channels.Tcp; using System.Runtime.Remoting.Activation; namespace RemoteSample public class ClientApp public static void Main(string [] arg) TcpChannel tcpChannel = new TcpChannel ();. tW-yjS sates
TCP 191 Channelservices.Registerchannel(tcpChannel); , И метод 1 . . .. tffeiiijs e ObjectEl attrs = ,(new . т .". , Жз®' | > ««₽•»» J>>. - t UrlAttribute(“tcp://localhost:8080/EchoMessage'J}; t ObjectHandle handle «= Activator. Createlnstance( “ServerObj”., x * ; *' “RemoteSawle-ServerObj", atVsfc a ServerObj obj « (ServerObj)handle UnwrapO; «• Console.WriteLine(“Client:" + obj.ServerMethod < (“Wrox Remoting Sample”)); // Метод 2 RemptingConfiguration.RegisterActivatedClientType( •^><4: ,-y x typeof(RemoteSample.ServerObj), “tcp-//localhost.8080/EchoMessage”); >4 ServerObj obj2 =new ServerObjO; > I Console. WriteLine(“Client2;” + obj2.ServerMethod( ’ к -•/£, s>'. “Wrox Remoting Sample’*)); > } ЦЛ Zi Хотя этот пример приложения действительно выполняет все основные действия, которые необходимо совершить при работе с .NET Remoting, осталась масса возможностей для усовершенствования этого приложения. Можно заметить, что в коде отсутствует проверка ошибок. Специально для среды Remoting создан класс RemotingException, весьма полезный в большинстве неприятных ситуаций, Встречающихся при разработке таких приложений. Поскольку канал TCP для сое- динения с сервером использует класс Socket, то, если сервер не слушает указанный порт, порождается исключение SocketException. Если вы забыли задать для удален- ного класса наследование от MarshalByRefObject, клиентский код породит исключе- ние RemotingException при вызове GetObject() с целью создания прокси-объекта для выполняемого удаленного объекта. То же исключение порождается при попытке регистрировать уже зарегистрированный канал. В этом случае вы можете вызвать метод Channelservices. GetChannelO, который вернет интерфейс канала, если канал уже зарегистрирован, и null в противном случае. Итоги В этой главе мы рассмотрели ядро архитектуры TCP, а также извлечение и фор- мирование его данных. Затем исследовали приложения клиент-сервер, построен- ные с использованием классов высокого уровня TcpClient и TcpListener. Чтобы продемонстрировать мощь класса TcpClient, мы создали полнофункциональное почтовое приложение-клиент. Кроме того, опираясь на поддержку классов много- поточной обработки в .NET, построили многопоточный эхо-сервер. Затем мы рассмотрели среду .NET Remoting Framework и, в частности транс- портный канал TcpChannel, имеющийся в .NET Framework. Наконец, продемонстри- ровали простое приложение среды Remoting, создающее из удаленного клиента экземпляр объекта на сервере.
ГЛАВА 6 UDP D Lhf главе 4 было рассмотрено программирование сокетов в .NET и рассказано, как можно использовать класс Socket для соединения с удаленными хостами через разные протоколы. В предыдущей главе мы исследовали классы TcpClient и TcpLis- tener, обеспечивающие высокоуровневую реализацию для соединения через TCP. Среда Microsoft .NET Framework также предоставляет специальный класс UdpClient для реализации User Datagram Protocol (UDP). В данной главе познакомимся с осно- вами протокола UDP и затем узнаем, как пользоваться классом UdpClient. В предыдущей главе описывалось “трехфазное квитирование”, которое исполь- зует протокол TCP, чтобы гарантировать корректную передачу данных. Хотя это средство делает TCP гораздо более надежным протоколом, оно также значительно повышает накладные расходы. В протоколе UDP нет ничего подобного, поэтому он работает гораздо быстрее и поэтому хорошо приспособлен для передачи таких мультимедийных данных, как видеоизображения, где точный порядок прибытия пакетов в пункт назначения может быть несущественным. На самом деле протокол UDP исключительно прост, и вся его спецификация (RFC 768) поместилась всего лишь на трех страницах! (Сравните ее со специфика- цией TCP, RFC 793, включающей 85 страниц.) В этой главе рассмотрим следующие темы: □ Основные понятия UDP □ Преимущества и недостатки протокола UDP □ Реализация протокола UDP в .NET с использованием класса UdpClient О Высокоуровневые протоколы, базирующиеся на UDP Обзор протокола UDP User Datagram Protocol (UDP)—это простой, ориентированный на дейтаграммы протокол без организации соединения, предоставляющий быстрое, но необяза- тельно надежное транспортное обслуживание. Он поддерживает взаимодействия “один со многими” и поэтому часто применяется для широковещательной и группо- вой передачи дейтаграмм.
UDP 193 Internet Protocol (IP) является основным протоколом Интернета. Transmission Control Protocol (TCP) и UDP—это протоколы транспортного уровня, построенные поверх лежащего в основе протокола. На следующем рисунке показано, как модель OSI соответствует архитектуре TCP/IP и набору протоколов TCP/IP: Представительский Прикладной Стек протоколов TCP/IP ' п_______ 7 уровней OSI tcp/ip? v,w "Е ....... Прикладной 7: 6 ‘«J 5 Сеансовый UDP JGMPli Канальный ЕС i_______-_______ ф | Физический л .<............... Сетевой Ethernet, ATM, Frame Relay ит. л. Транспортный Сетевой Транспортным TCP Интернет TCP/IP — это набор протоколов, называемый также “пакетом протоколов Интернета” (Internet Protocol Suite), состоящий из четырех уровней. Запомните, что TCP/IP не просто один протокол, а семейство или набор протоколов, который состоит йз других низкоуровневых протоколов, таких, как IP, TCP и—предмета дан- ной главы — UDP. UDP располагается на транспортном уровне поверх IP (протоко- ла сетевого уровня). Транспортный уровень обеспечивает взаимодействие между сетями через шлюзы. В нем используются IP-адреса для отправки пакетов данных через Интернет или другую сеть с помощью разнообразных драйверов устройств. TCP и UDP входят в набор протоколов TCP/IP, каждый из них имеет свои преи- мущества и недостатки, которые обсудим позднее в этой главе. Некоторая терминология UDP Прежде чем приступать к изучению работы UDP, обратимся к основной терми- нологии, которую нужно хорошо знать. В этом разделе вкратце определим основ- ные термины, связанные с UDP. Пакеты В передаче данных пакетом называется последовательность двоичных цифр, представляющих данные и управляющие сигналы, которые передаются и коммути- руются через хост. Внутри пакета эта информация расположена в соответствии со специальным форматом. Дейтаграммы Дейтаграмма — это отдельный, независимый пакет данных, несущий информа- цию, достаточную для передачи от источника до пункта назначения, поэтому ника- кого дополнительного обмена между источником, адресатом и транспортной сетью не требуется. MTU ч MTU означает Maximum Transmission Unit (максимальный блок передачи). MTU характеризует канальный уровень и соответствует максимальному числу бай-
194 Глава 6 *W*»‘****"‘«*M«****W*W****’**W«H«imaW^^ тов, которое можно передать в одном пакете. Другими словами MTU — это самый большой пакет, который может переносить данная сетевая среда. Например, Ether- net имеет фиксированный MTU, равный 1 500 байтам. В UDP, если размер дейтаг- раммы больше MTU, протокол IP выполняет фрагментацию, разбивая дейтаграмму на более мелкие части (фрагменты) так, чтобы каждый фрагмент был меньше MTU. Порты Чтобы поставить в соответствие входящим данным конкретный процесс, вы- полняемый в компьютере, UDP использует порты. UDP направляет пакет в со- ответствующее место, используя номер порта, указанный в UDP-заголовке дейтаграммы. Порты представлены 16-битными номерами и, следовательно, при- нимают значения в диапазоне от 0 до 65 535. Порты, которые также называют ко- нечными точками логических соединений, разделены на три категории: □ Хорошо известные порты — от 0 до 1 023 О Регистрируемые порты — от 1 024 до 49 151 О Динамические/частные порты — от 49 152 до 65 535 Заметим, что порты UDP могут получать более одного сообщения в каждый про- межуток времени. В некоторых случаях сервисы TCP и UDP могут использовать одни и те же номера портов, например 7 (Echo) или 23 (Telnet). UDP использует следующие известные порты: Номер порта UDP Описание 15 NETSTAT—Состояние сети 53 DNS — Сервер доменных имен 69 ТЕГР — Простейший протокол передачи файлов 137 Служба имен NetBIOS 138 Дейтаграммная служба NetBIOS 161 SNMP Перечень портов UDP и TCP поддерживается агентством IANA (Internet Assig- ned Numbers Authority). Подробную информацию о назначенных портах можно найти на странице http://www.iana.org/assignments/port-numbers. IP-адреса Дейтаграмма IP состоит из 32-битных IP-адресов источника и назначения. 1Р-ад- рес назначения задает конечную точку для дейтаграммы UDP, а IP-адрес источника используется для получения информации о том, кто отправил сообщение. В пункте назначения пакеты фильтруются, и те из них, адреса источников которых не входят в допустимый набор адресов, отбрасываются без уведомления otj [равителя. Однонаправленный IP-адрес уникально определяет хост в сети, тогда как груп- повой JP-адрес определяет конкретную группу адресов в сети. Широковещательные IP-адреса получаются и обрабатываются всеми хостами локальной сети или кон- кретной подсети.
UDP 195 IP-адреса делятся на пять классов, показанных в следующей таблице: IP-класс Диапазон IP-адресов Использование Class А От 0.0.0.0 до 127.255.255.255 Сети с большим числом хостов, например сети крупных международных организаций Class В От 128.0.0 0 до 191.255.255.255 Сети со средним числом хостов, принадлежащие, например, университетам. Class С От 192.0.0.0 до 223.255.255.255 Сети с небольшим числом хостов, принадлежащие, например, небольшим компаниям Class D От 224.0.0.0 до 239.255.255.255 Используются в сетях с групповой рассылкой, например каналах новостей реального времени Class Е От 240.0.0.0 до 247.255.255.255 Резервирован для экспериментов Несколько IP-адресов ограничены специальным применением: IP-адрес Использование 0.0.0.0 IP-адрес хоста по умолчанию (любой доступный интерфейс) 127.0.0.1 IP-адрес локальной обратной связи 255.255.255.255 Широковещательный IP-адрес для всей локальной сети TTL Значение времени жизни, или TTL (fwn^to-Zive), позволяет установить верхний предел числа маршрутизаторов, через которые может пройти дейтаграмма. Значе- ние TTL не дает пакетам попасть в бесконечные циклы. Оно инициализируется отправителем и уменьшается на единицу каждым маршрутизатором, обрабатываю- щим дейтаграмму. Когда значение TTL становится нулевым, дейтаграмма отбрасы- вается. Групповоя рассылка Групповая рассылка — это открытый, базирующийся на стандартах метод одно- временного распространения идентичной информации нескольким пользова- телям. Групповая рассылка является основным средством протокола UDP, она невозможна для протокола TCP. Групповая рассылка позволяет добиться взаимо- действия одного со многими, например, делает возможными такие использования, как рассылка новостей и почты нескольким получателям, интернет-радио или де- монстрационные программы реального времени. Групповая рассылка не так сильно нагружает сеть, как широковещательная передача, поскольку данные от- правляются сразу нескольким пользователям:
196 Главаб В следующей azаве расе vo,прим групповую рассылку детально. Как работает UDP Когда приложение, базирующееся на UDP, отправляет данные другому хосту в сети, UDP дополняет их восьмибайтным заголовком, содержащим номера портов адресата и отправителя, общую длину данных и контрольную сумму. Поверх дейта- граммы UDP свой заголовок добавляет IP, формируя дейтаграмму IP: Общая длина (16 битов) Контрольная сумма заголовка (16 битов) IP-адрсс источника (32 бита) IP-адрес назначения (32 бита) Флаги Смещение фрагмента (3 бита) (13 бигов) 15116 Заголовок IP UDP Номер порта источника (16 битов) Длина UDP (16 битов) Л. ' '.?* . ,8 ’— Номер порта назначения (16 битов) Контрольная сумма UDP (16 битов) w •Ml; V’' *' WaW"'” 'П ’’KST Протокол (S twos) О 3 ' 7 я Длина Тип .1 заголовка обслуживания "114 бига) (8 битов) Идентификация (16 битов) На предыдущем рисунке указано, что общая длина заголовка UDP составляет восемь байтов. Теоретически максимальный размер дейтаграммы IP равен 65 535 байтам. С учетом 20 байтов заголовка IP и 8 байтов заголовка UDP длина данных пользова- теля может достигать 65 507 байтов. Однако большинство программ работают
UDP 197 с данными меньшего размера. Так, для, большинства приложений по умолчанию установлена длина приблизительно 8192 байта, поскольку именно такой объем данных пользователя по умолчанию считывается и записывается сетевой файловой системой (NFS). Можно устанавливать размеры входного и выходного буферов. Контрольная сумма нужна, чтобы проверить, были ли данные доставлены в пункт назначения правильно или были искажены. Она охватывает как заголовок UDP, так и данные. Байт-заполнитель используется, если общее число октетов дей- таграммы нечетно. Если полученная контрольная сумма равна нулю, получатель фиксирует ошибку контрольной суммы и отбрасывает дейтаграмму. Хотя контроль- ная сумма является необязательным средством, ее всегда рекомендуется включать. Заметим, что контрольную сумму нельзя подключать и отключать в классе UdpCl lent. Чтобы это сделать, надо обратиться к низкоуровневому классу Socket (см. главу 4) ичерез метод Set Socket Opt ion () установить опцию NoChecksum. На следующем шаге уровень IP добавляет 20 байтов заголовка, включающего TTL, IP-адреса источника и получателя и другую информацию. Это действие назы- вают 1Р-инкапсуляцией. Как упоминалось ранее, максимальный размер пакета равен 65 507 байтам. Если пакет превышает установленный по умолчанию размер MTU, то уровень IP раз- бивает пакет на сегменты. Эти сегменты называются фрагментами, а процесс разбиения данных на сегменты — фрагментацией. Заголовок IP содержит всю ин- формацию о фрагментах. Когда приложение-отправитель “выбрасывает” дейтаграмму в сеть, она направ- ляется по IP-адресу назначения, указанному в заголовке IP. При проходе через мар- шрутизатор значение времени жизни (TTL) в заголовке IP уменьшается на единицу. Когда дейтаграмма прибывает к заданному назначению и порту, уровень IP по своему заголовку проверяет, фрагментирована ли дейтаграмма. Если это так, дей- таграмма собирается в соответствии с информацией, имеющейся в заголовке. На- конец прикладной уровень извлекает отфильтрованные данные, удаляя заголовок. Недостатки UDP По сравнению с TCP UDP имеет следующие недостатки: □ Отсутствие сигналов квитирования. Перед отправкой пакета UDP отправ- ляющая сторона не обменивается с получающей стороной квитирующими сигналами. Следовательно, у отправителя нет способа узнать, достигла ли дейтаграмма оконечной системы. В результате UDP не может гарантировать, что данные будут действительно доставлены адресату (например, если не ра- ботает оконечная система или сеть). Напротив, протокол TCP ориентирован на установление соединений и обес- печивает взаимодействие между подключенными к сети хостами, используя пакеты. В TCP применяются сигналы квитирования, позволяющие прове- рить успешность транспортировки данных. □ Использование сессий. Ориентированность TCP на соединения поддержи- вается сеансами между хостами. TCP использует идентификатор сеанса, по- зволяющий отслеживать соединения между двумя хостами. UDP не имеет поддержки сеансов из-за своей природы, не ориентированной на соедине- ния. О Надежность. UDP не гарантирует, что адресату будет доставлена только одна копия данных. Чтобы отправить оконечной системе большой объем данных,
198 Глава 6 UDP разбивает его на небольшие части. UDP не гарантирует, что эти части бу- дут доставлены по назначению в том же порядке, в каком они создавались в источнике. Напротив, TCP вместе с номерами портов использует порядко- вые номера и регулярно отправляемые подтверждения, гарантирующие упо- рядоченную доставку данных. □ Безопасность. TCP более защищен, чем UDP. Во многих организациях бранд- мауэры и маршрутизаторы не пропускают пакеты UDP. Это связано с тем, что хакеры могут воспользоваться портами UDP, не устанавливая явных соеди- нений. □ Управление потоком. В UDP управление потоком отсутствует, в результате плохо спроектированное UDP-приложение может захватить значительную часть пропускной способности сети. Преимущества UDP □ Нет установки соединения. UDP является протоколом без организации сое- динений, поэтому он освобождает от накладных расходов, связанных с уста- новкой соединений. Поскольку UDP не пользуется сигналами квитирования, то задержек, вызванных установкой соединений, также удается избежать. Именно поэтому DNS отдает предпочтение UDP перед TCP — DNS работала бы гораздо медленнее, если бы она выполнялась через TCP. □ Скорость. UDP работает быстрее TCP. По этой причине многие приложения предпочитают не TCP, a UDP. Те же средства, которые делают TCP более устойчивым (например сигналы квитирования), замедляют его работу. □ Топологическое разнообразие. UDP поддерживает взаимодействия “один с одним” и “один с многими”, в то время как TCP поддерживает лишь взаимо- действие “один с одним”. О Накладные расходы. Работа с TCP означает повышенные накладные расхо- ды, издержки, налагаемые UDP, существенно ниже. TCP пр сравнению с UDP использует значительно больше ресурсов операционной системы, и, как следствие, в таких средах, где серверы одновременно обслуживают многих клиентов, широко используют UDP. □ Размер заголовка. Для каждого пакета заголовок UDP имеет длину всего лишь восемь байтов, в то время как TCP имеет 20-байтовые заголовки, и поэ- тому UDP потребляет меньше пропускной способности сети. В следующей таблице суммируются различия между TCP и UDP: Характеристика UDP TCP Ориентированность на соединения Нет Да Использование сеансов Нет Да Надежность Нет Да Подтверждения Нет Да Упорядоченность Нет Да Управление потоком Нет Да Безопасность Ниже Выше
UDP 199 продолжение таблицы Контрольная сумма данных Необязательно Да Накладные расходы Меньше Больше Скорость Высокая Низкая Топология Один с одним Один с одним - Один со многими Заголовок 8 байтов 20 байтов Когда использовать UDP В Интернете многие приложения используют UDP. UDP известен, как протокол “оптимальных усилий”. Рассмотрев преимущества и недостатки UDP, можно сде- лать вывод, что к этому протоколу полезно обращаться в следующих ситуациях: □ Для широковещательной и групповой передачи, если приложению нужно взаимодействовать с несколькими хостами □ Если размеры дейтаграмм невелики, а последовательность фрагментов не слишком существенна □ Если установление соединения не требуется □ Если приложению не нужно обмениваться значительными объемами данных (поскольку в UDP нет управления потоком) □ Если повторная передача пакетов не нужна О Если операционная система требует низких накладных расходов □ Если пропускная способность сети является критичным фактором UDPb.NET В .NET протокол UDP можно реализовать, используя следующие возможности: □ Класс UdpCIient t □ Класс Socket □ Элемент управления Winsock □ Неуправляемый API Winsock Последние два пункта опираются на COM-интероперабельность и P/Invoke со- ответственно и в этой книге не рассматриваются. Пространство имен Sys- tem, Net. Sockets по существу является оболочкой для Winsock API, поэтому предпочтительнее использовать классы .NET. Элемент управления Winsock может оказаться хорошим выбором для тех, кто программировал на Visual Basic и хочет поддерживать визуальность своего программного обеспечения, но эта возмож- ность увеличивает издержки из-за COM-интероперабельности, поэтому лучше ее из- бегать. В главе 4 было рассмотрено использование класса Socket. Отметим, что этот класс по сравнению с высокоуровневым классом UdpCIient дает доступ к большему числу опций (например, позволяет отключить контрольную сумму, о чем упомина- лось ранее), но зато программный код несколько усложняется. Однако, как будет по- казано далее, класс UdpCIient построен поверх класса Socket, и, наследуя UdpCIient, можно обращаться к базовому классу Socket.
200 Глава 6 В этой главе рассмотрим реализацию UDP с использованием класса UdpClient. Прежде чем приступить к этой реализации, пользователь должен познакомиться с некоторыми другими основными классами платформы .NET и понять основы ра- боты с сокетами в .NET. Эти темы обсуждались в главах 3 и 4. Классы .NET для работы с UDP находятся в пространстве имен System. Net. Soc- kets. Это пространство имен предоставляет управляемые классы для TCP, UDP и общего программирования сокетов: System.Object 4, Syrt*m.Net.Sock»t» —~ Socket ' '’ £££- fcpClieht * j -к ’ IcpUstent' E ' * Ф UdpClient Класс UdpClient Среда Microsoft .NET Framework предоставляет класс UdpClient для реализации в сети протокола UDP. Как и классы TcpClient и TcpListener, этот класс построен на классе Socket, но скрывает излишние члены, которые не требуются для реализации приложения, базирующегося на UDP. Применять класс UdpClient довольно просто. Во-первых, создайте экземпляр UdpClient. Далее через вызов его метода Connect () соединитесь с удаленным хостом. Эти два шага можно сделать в одной строке, если указать в конструкторе UdpClient удаленный IP-адрес и удаленный номер порта. Ранее было сказано, что протокол UDP не ориентирован на установление соединений, поэтому может возникнуть воп- рос: так зачем же этот Connect? В действительности метод Connect () до отправки или получения данных не устанавливает соединение с удаленным хостом. Когда вы отправляете дейтаграмму, то пункт назначения должен быть известен, для этого нужно указать IP-адрес и номер порта. Третий шаг состоит в отправке и получении данных с использованием метода Send() или Receive(). Наконец метод CloseO закрывает соединение UDP. Все эти шаги иллюстрируются на следующем рисунке:
UDP 201 Методы и свойства UdpCIient На следующей схеме показаны методы и свойства класса UdpCIient. Здесь не бу- дут обсуждаться методы и свойства, унаследованные от System. Object. Следующие методы и свойства рассмотрим детально в ходе обсуждения класса UdpCIient: Создание экземпляра класса UdpCIient Экземпляр класса UdpCIient можно создать несколькими способами, которые от- личаются передаваемыми параметрами. Наше использование объекта UdpCIient за- висит от того, как он создавался. Простейший способ состоит в вызове конструктора по умолчанию (без переда- чи параметров). Когда экземпляр класса создается так, нужно или вызвать метод Connect () и установить соединение, или задать информацию о соединении при от- правке данных. /У Создаем экземпляр UdpCIient, используя конструктор по умолчанию UdpCIient UdpCIient = new UdpClientO; Если выберем этот конструктор, то будут использоваться произвольный свобод- ный порт и IP-адрес 0.00.0. Можно также создать объект UdpCIient, указав в параметре номер порта. В этом случае UdpCIient будет слушать все локальные интерфейсы (т. е. использовать IP-ад- рес 0 0.0.0). Если номер порта находится вне пределов, указанных полями MinPort и MaxPort класса IPEndPoint, порождается исключение ArgumentOutOfRangeException (производное от ArgumentException). Если указанный порт уже занят, порождается исключение SocketException. // Создаем UdpCIient, используя номер порта * try { UdpCIient UdpCIient = new UdpCIient(5001); catch (ArgumentOutOfRangeException e) t - 4 " : W ' Console.WriteLine(“Invalid port number”); catch (SocketException e) Console.WriteLine(“Port Is already in use’’);
202 Глава 6 Следующий способ заключается в использовании объекта IPEndPoint, представ- ляющего локальный IP-адрес и номер порта, которые хотим выбрать для соедине- ния. В этом случае первый шаг состоит в создании экземпляра класса JPEndPoint, а это можно сделать, использовав длинный IP-адрес или объект IPAddress. IP-адрес должен принадлежать одному из интерфейсов локальной машины, иначе будет по- рождено исключение SocketException с ошибкой "The requested address is not valid in its context' (Запрошенный адрес в этом контексте неприменим). Если конструктору передается пустой объект IPEndPoint, порождается исключе- ние ArgumentNullException. // Создаем экземпляр IPEndPoint. IPAddress ipAddress fe IPAddress.Parse("127.0.0.1”) ; '!’Л 1 IPEndPoint ipLocalEndPoint = new IPEndPoint(ipAddress, 5001); // Используем экземпляр IPEndPoint для создания объекта UdpClient UdpClient UdpClient = new UdpClient(ipLocalEndPoint); / ’ •” ! catch (Exception e) Console.WriteUne(e.ToStringO) ; t . Последний способ состоит в передаче конструктору имени хоста и номера пор- та. В этом случае конструктор инициализируется именем хоста и номером порта удаленного хоста. Это позволяет исключить шаг с вызовом метода Connect(), по- скольку он вызывается из конструктора (как можно видеть, если вызвать ILDasm и исследовать IL-код конструктора). // Создаем объектипрСЦепх, используя имя удаленного хоста и номер порта/ л 7 .. ....-f й ~ : к. > я -Г' ‘ UdpClient UdpClient = new UdpClientC'remoteHostName”, 5001); catch (Exception e) Console.WriteLine(e.ToSt ri ng()); Определение информации о соединении После создания объекта UdpClient переходим ко второму шагу — подготовке ин- формации о соединении, которая будет использоваться, когда потребуется отпра- вить данные удаленному хосту. Вспомните, что протокол UDP не ориентирован на соединения и эта информация не нужна для получения данных, она используется, только чтобы указать, куда мы хотим отправить данные. Эту информацию можно указать в любом из следующих трех мест: как мы уже ви- дели, ее можно задать в конструкторе UdpCl ient, можно явно вызвать метод Connect () класса UdpClient или ее можно включить в метод Send() при фактической передаче данных. Существуют три перегруженных метода Connect (): □ Использующий объект IPEndPoint □ Устанавливающий соединение, используя IP-адрес и номер порта удаленного хоста □ Использующий имя DNS или машины и номер порта удаленного хоста
UDP 203 Использование объекта IPEndPoint Первый перегруженный метод Connect () для соединения с удаленным хостом ис- пользует экземпляр класса IPEndPoint, поэтому перед вызовом метода Connect() со- здаем объект IPEndPoint. Если при соединении возникают какие-либо ошибки, порождается исключение SocketExceptidn. // Создаем объект UdpClient UdpClient UdpClient •= new UdpClient(); // Получаем IP-адрес удаленного хоста IPAddress ipAddress * IPAddress.Parse(“224 56.C. V); // Создаем объект IPEndpoint, используя IPAddress и номер порта IpEndPoint . ipEndPoint = new iPEndPoint(ipAddress, 1234); л * ... <- ’ ф try .< , ж: • V J b. { .Г . - 7 ' // Соединение с использованием этого объекта IPEndPoint UdpClient.Connect(IpEndPoint) ; Л } /Ж* catch (Exception e) { Console.WriteLine(“Error while connecting: " + e.ToStringO); Использование IPAddress и номера порта Второй перегруженный метод принимает объект IPAddress и номер порта уда- ленного хоста. Если известны удаленный IP-адрес и удаленный UDP-порт, можно со- здать соединение с удаленным UDP-хостом, как показано в следующем примере: // Создаем экземпляр UdpClient UdpClient UdpClient = new UdpClientO; // Получаем IP-адрес удаленного хоста IPAddress ipAddress = IPAddress.Parse(“224.56.0.1"); try { - - // Соединяемся, используя созданный объект IPAddress и удаленный порт UdpClient.Connect(ipAddress, 1234); } catch (Exception e ) { Console.Writeline(“Error while connecting: * + e.ToStringO) ; Использование имени хоста и номера порта В последнем перегруженном методе используется DNS-имя и номер порта уда- ленного хоста. Это довольно простой метод, поскольку не нужно создавать ни объ- ект IPAddress, ни объект IPEndPoint: // Создаем экземпляр класса UdpClient UdpClient UdpClient s new UdpClientO; try { UdpClient.Connect(“remoteMachineName", 1234); } catch (Exception e) ,:;Л 7:*> «к ... -л .. /& JR. fr/.,1. .w.7
204 Глава 6 Г^Г* { .—..•§ «i. e Ж ♦ !* ?• „ Console.WriteLine(“Error i' while. '-'connecting: ” | + e.ToString()); 1 -J у ййИ Z • -. •. ?. '• -V. ' - J< *' ? ' v ? Отправка данных через объект UdpCIient Получив экземпляр класса UdpCIient и подготовив (необязательную) информа- цию о соединении, можно приступить к отправке данных. Неудивительно, что для этого вызывается метод Send(), который используется, чтобы послать дейтаграмму от клиента удаленному хосту. Важный момент, характеризующий протокол UDP, со- стоит в том, что после отправки данных удаленному хосту он не получает никаких подтверждений. Как и метод Connect (), метод Send() представлен несколькими пере- груженными методами. Send () возвращает длину данных, которую можно использо- вать для проверки, правильно ли были отправлены данные. Основная процедура отправки данных для класса UdpCIient показана на следую- щей схеме: Отправка данных с использованием UDP включает следующие четыре шага: 1. Создать экземпляр UdpCIient ' 2. Соединиться с удаленным хостом (необязательно) 3. Отправить данные 4. Закрыть соединение Прежде чем рассмотреть метод Send() детально, задержим внимание на этом примере, показывающем общий процесс: /7 Пример: отправка данных . private static void Send(string datagram) { v \ - vxF" Sir П Удаленный IP-адрес' •» / IPAddress remoteAddress - IPAddress.Parse("l?7 Ч Порт, с которым нужно соединиться г • int remotePort 's 5001; //«» ШАГ 1 »* Создаем объект UdpCIient VdpClient gender new UdpClientO; try
UDP 205 feg //. ** ШАГ 2 ** Соединяемся' с удаленным хостом sender.Connect(remoteAddress. remotePort); . Console.writeLine(“Sending datagram: 0", datagram); byte[] bytes'1-* Encoding.ASCII.GetBytes(datagram); l/Л ШАГ 3 ** Посылаем данные соединенному хосту sender Send(bytes, bytes.Length); * ' // *> ШАГ 4 »* Закрываем соединение sender. Closet); I catch Exception e) .. Console.WriteLinete.TPSt ring()); Метод SendO Хотя в этом примере показан общий подход, метод Send() можно использовать самыми разнообразными способами в зависимости оттого, как UdpClient был соеди- нен с удаленным портом и как создавался экземпляр класса UdpClient. Если перед вызовом метода SendO информация о соединении не была определена, нужно ее включить в этот вызов. Первый перегруженный метод принимает три параметра: данные в массиве бай- тов, длину данных в поле int и объект IPEndPoint. Чтобы использовать этот пе- регруженный метод SendO, сначала создадим экземпляр класса IPEndPoint, задав конечную точку, с которой хотим соединиться, — IP-адрес и номер порта. Затем пе- редаем этот объект IPEndPoint в метод Send(). Имейте в виду, что мы не можем обра- щаться к этому варианту метода SendO, если явно вызывали метод Connect() или передали информацию о соединении конструктору UdpClient. Следующий пример иллюстрирует, как отправить данные, используя объект IPEndPoint: // Создаем экземпляр UdpClient. Заметим, как он создается. UdpClient UdpClient = new UdpClientО; ff Получаем удаленный IPAddress IPAddress ipAddress * IPAddress.Parse(*’148.182.27.1”); // Создаем экземпляр IPEhdPoiht, передав IP-адрес и удаленный порт V / IPEndPolnt ipEndPoint ₽ new IPEndPoint(ipAddres$, 5005); // Создаем данные в формате Byte[l < byte[] sendBytes = Encoding.ASCII.GetBytes(“Wrox UDP Send Example”); try < /7 Отправляем данные, используя экземпляр IPEndPoint. UdpClient.Send(sendBytes, sendBytes.Length, ipEndPoint); catch (Exception e) Console.WriteLine (“Error: . ” + e.ToStringOИ ; J ’ . : .1?*' Второй перегруженный метод позволяет задать имя хоста и номер порта удален- ной конечной точки, а также данные и их длину в байтах. И в этом случае метод Send() предполагает, что между клиентом и удаленным хостом соединение не было
206 Глава 6 установлено, поэтому его нельзя вызвать, если уже вызван метод Connect () или указа- на информация о соединении в конструкторе UdpCIient. // Создаем экземпляр UdpCIient. Заметим, как он создается. 4 W UdpCIient UdpCIient = new UdpClientO; // Создаем данные в формате byte[] byte[] sendBytes = Encoding.ASCII GetBytes("Wrox UDP Send Example”); '2. try ' ' < {> •« ' 1 i •<-- A *• . ... , - Л- <4 ,/-*«' - . i. .- // Отправляем данные, указав имя удаленной машины и удаленный порт UdpCIient,Send(sendBytes, sendBytes.Length, “remoteHostName", 5001); catch (Exception e) Console WriteLine("Error: " + e.ToStringQ); • ; } Последний перегруженный метод предполагает, что клиент UDP уже соединен с удаленным хостом, поэтому остается только указать методу Send() данные и поле int, представляющее длину данных. Это единственная версия метода Send(), кото- рую можно использовать в сочетании с методом Connect (). , // Создаем экземпляр UdpCIient. Замечаем, как он создается. '>• UdpCIient UdpCIient s new UdpCIient(“remoteHostName”, . 5001); « // Создаем данные в формате Byte[] л > bytef] sendBytes - Encoding. ASCII.GetBytes(“WROX UDP Send Example").: try - U Отправляем данные UdpCIient.Send(sendBytes, sendBytes.Length); / r ' - . .. • ' catch (Exception e) Console WriteLine( "Error: '* + e,ToString()), Получение донных с использованием объекта UdpCIient Естественно, что для получения данных от удаленного хоста через UDP вызыва- ется метод Receive (). Этот метод принимает один ссылочный параметр, экземпляр класса IPEndPoint, и возвращает в массиве байтов принятые данные. Обычно реко- мендуется выполнять этот метод в отдельном потоке, поскольку он опрашивает ба- зовый сокет на предмет поступления дейтаграмм и блокирует поток, пока данные не будут получены. Если он выполняется в основном потоке, выполнение програм- мы приостанавливается, пока не будет получен пакет дейтаграммы. Если объекту UdpCIient уже указана информация о соединении в конструкторе или вызван метод Connect (), то метод Receive () будет принимать и возвращать наше- му приложению только данные от указанной удаленной точки, а соединения от дру- гих источников будут отбрасываться. Если никакая информация о соединении не задавалась, будут приниматься все входящие соединения с локальной конечной точ- кой. После получения дейтаграммы этот метод возвращает данные в массиве байтов (удалив информацию заголовка) и заполняет объект IPEndPoint, на который ссыла- ется параметр, информацией об удаленном хосте, отправившем данные. Процесс получения данных от удаленного хоста очень похож на отправку дан- ных:
UDP 207 Для создания приложения-приемника дейтаграмм UDP нужно выполнить следу- ющие общие шаги: 1. Создать экземпляр класса UdpClient. 2. Получить данные. 3. Закрыть UdpClient. Все три шага показаны в следующем примере. Шаг 2 начинается, когда UdpClient хочет получить данные*. ' 7/ Пример: получение данных private static Void ReceiveQ . .. .. ' - /7 ** ШАГ 1 ** Создаем UdpClient для считывания входящих данных. f UdpClient receivingUdpClient = new UdpClient(SOOT); // Слушает на порте 5001 V.' , 'Ч ' // Создаем переменную IPEndPoint, чтобы передать ссылку на нее в ReceiveO IPEndPoint RemotelpEndPoint = null;. w !' try Console.WriteLine("Listeriing on port 5001.. Л); // ♦♦ ШАГ 2 ** Без потока блокируем программу, •?,» т - • пока сокет не закончит // получать данные byted receiveBytes » receivingUdpClient.Receive (ref RemotelpEndPoint); // Преобразуем данные ’ " string returnData » Encoding.ASCII.GetString(receiveBytes); Console.WriteLine(“0y bytes received from 1”, receiveBytes.Length, RemotelpEndPoint.ToString()); Console.WriteLine(retd rnData); // ♦* ШАГ 3 ** Закрываем UdpClient receivingUdpClient.Closet); catch (Exception e) { Console.WriteLine(e.ToString());
208 Глава 6 Закрытие соединения Последний шаг при работе с UdpClient состоит в закрытии соединения. Для за- крытия открытого UDP-соединения используется метод CloseO. Если при закры- тии соединения возникают какие-либо ошибки, порождается исключение SocketException. Закрыть соединение вовсе не трудно: " UdpClient,CloseO I Методы групповой рассылки Мы не собираемся в этой главе глубоко исследовать групповую рассылку, поскольку следующая глава посвящена исключительно этой теме. Однако ради полноты картины стоит упомянуть о двух методах класса UdpClient, которые исполь- зуются этим средством. Метод JoinMulticastGroupO Этот метод позволяет присоединиться к группе. С этим методом объект UdpClient может получать групповые дейтаграммы, рассылаемые по указанному IP-адресу. Групповая рассылка позволяет доставлять данные по нескольким назначениям. Она характерна для протокола UDP и широко распространена в мире Интернета. Метод JoinMulticastGroupO позволяет присоединиться к групповому IP-адресу. Существуют две перегруженные версии этого метода. Первая принимает только об- ъект IPAddress, представляющий IP-адрес группы, к которой мы хотим присоеди- ниться. Объект UdpClient будет получать любые дейтаграммы, рассылаемые по этому IP-адресу: // Создаем экземпляр UdpClient UdpClient UdpClient = new UdpClientOЛ // IPAddress руппь IPAddress fflulticastIP = IPAddress ParseC'224.123.32.64")? try ..... ’ // Присоединяемся k группе UdpClient. JoinMulticastGroup(mult3 castIP); I catch {Exception e) Console. WriteLine(e. ToSt ringQ): } Второй перегруженный метод принимает групповой IP-адрес со значением TTL в поле int: UdpClient UdpClient ₽ new UdpClientO: // Создаем IPAddress для присоединения IPAddress _ multIcastIP = Dns.Resolve(‘‘niutriCastHost”).AddressLxstrO]; -A& V 7 ^7 ‘ -1 'i w t { ‘ , > • // Г.(изнь пакета равна 3U “прыжкам” через маршрутизаторы. UdpClient.JoinMuliicastGroupCmulticastlP, 30); . }’’ ’У < * catchy {.Exception е) =>=..
UDP 209 { Console.WriteLine(e.ToSt ring()); Чтобы отправить групповую дейтаграмму, укажите групповой IP-адрес в диапазоне от 224.0,0, Одо 239.255.255.255. ч Метод DropMulticastGroupO Этот метод можно использовать, чтобы отсоединить объект UdpClient от груп- пы. Этот метод принимает один параметр — IP-адрес группы, которую этот клиент должен оставить: try { // Оставляем группу, посылая экземпляр объекта IPAddress UdpClient. DropMulticaStGroup(fnulticastlP) ; } catch (Exception e) { J . Console WriteLine("Error while using DropMulticastGroup + e.ToStringO); Защищенные свойства В классе UdpClient есть два защищенных свойства, доступных из класса, в кото- ром они объявлены, и из производного класса. Это означает, что к защищенным свойствам нельзя напрямую обращаться из экземпляра UdpClient. Свойство Active Свойство Active используется для проверки соединения с удаленным хостом. Это свойство возвращает значение true, если соединение активно. Свойство Client Это свойство используется для получения базового объекта Socket, используемо- го объектом UdpClient. Как упоминалось ранее, класс UdpClient построен поверх класса Socket. Свойство Client позволяет обращаться к лежащему в основе сокету и, следовательно, всем членам класса Socket, недоступным через класс UdpClient. Это самое значительное преимущество этого свойства. Например, в свойстве Blocking класса Socket можно указать, находится ли Socket в блокирующем режиме. С по- мощью доступных членов класса UdpClient этого сделать нельзя. В следующем примере используются оба эти свойства — Active и Client: // Пример: использование свойств Active и Client using System, using System.Net; using System.Net.Sockets; class UdpDerived : UdpClient { public void insideSocketO . { . Г - - // Метод обращается к защищенному свойству Active, принадлежащему // базовому классу UdpClient,. чтобы определить наличие соединения # ‘"’if (this.Active) .
210 Глава 6 Console.WriteLine(“Connection is Active!4); // Получаем базовый объект Socket, чтобы иметь возможности доступа к // богатому набору всех методов и свойств класса Socket Socket richSock = this:Client; // например, следующее свойство возвращает тип сокета Console.WriteLine("Socket Type is: ’’ + richSock. SocketType. ToString()); . > - i ... [STAThread] ' static void Main(string[] args) UdpDerived derivedlnstance = new UdpDerivedO; // Соединяемся, чтобы проверить свойство Active IPEndPoint? endPoint = new IPEndPoint(IPAddress.Parse)"l27.0.0.r*l, г , .a ,5001); 1 . с,'. ’.«•»* .. ' ' V * // Соединение с использованием экземпляра производного классу derivedlnstance.Connect(endPoint) ; ? ~ Sr- j "--jr • • f // Вызываем защищенный метод derivedlnstance.insideSocket(); } ' • } Приложение интерактивного форума, использующее UDP Чтобы проиллюстрировать более конкретно объединение этих процессов, раз- работаем простое диалоговое приложение на языке С#, использующее класс UdpCIient. Поскольку мы уже написали примеры отправки и получения данных, то создать диалоговое приложение не составит труда. Диалоговое приложение использует отдельный поток, чтобы слушать сообще- ния от удаленных хостов. Класс Thread принадлежит пространству имен Sys- tem. Threading, поэтому включим в проект следующую директиву using: using System.Threading; Это приложение разделено на три логические части. В первой части пользовате- лю предлагается ввести информацию о локальных и удаленных портах и удаленном IP-адресе, которые он хочет использовать. На следующей схеме показана возмож- ная установка портов, позволяющаяпротестировать приложение на одной машине. Порт 5001 используется как отправляющий порт для хоста А и как приемный порт для хоста В, для порта 5002 все наоборот:
UDP 211 »tsV.»ft^t<W<Mi<»*4(>,«*W««S1<I«Mt»i4»fc«fc»MraV'"5<*^«**;’^^ RiWWW «WxA^r^v Лч^вПЙ ЧП ХЧ. -Л»' Во второй части приложение слушает входящие данные от удаленного хоста. Как обсуждалось ранее, метод Receive() проверяет наличие входящих дейтаграмм и блокирует поток, пока от удаленного хоста не поступит сообщение. Чтобы отделить этот процесс от основной последовательности действий, создается новый поток. Делегат Th readStart ссылается на метод Receive г (), который вызывается, когда стар- тует поток: Thread tRec = new Thread(new ThreadStart(Receiver)); tRec.Start(); В методе Receiver() создается экземпляр класса UdpClient с использованием ука- занного порта localPort: UdpClient receivingUdpClient = new UdpClient(localPort); Далее создаем новый экземпляр класса IPEndPoint и присваиваем ему значение null, чтобы можно было его передавать в метод Receive() как параметр-ссылку: IPEndPoint RemotelpEndPoint = null; Затем входим в бесконечный цикл while, ожидая получения данных методом Receive(). Данные возвращаются как массив байтов, которые снова преобразуем в исходную строку, используя класс Encoding. ASCII: while(true) { И Ждем дейтаграмму byte[] receiveBytes = receivingUdpClient.Receive (ref RemotelpEndPoint); // Преобразуем и отображаем данные string returnData = Encoding.ASCII.GetString(receiveBytes); Console.WriteLine(“-” + returnData); > Третий логический блок в приложении принимает данные, введенные пользо- вателем, и отправляет их указанному удаленному порту. Он выполняется на основ- ном потоке, пока рабочий поток продолжает слушать входящие данные. Для отправки данных удаленному порту на первом шаге создается экземпляр класса UdpClient: / UdpClient sender = new UdpClientO; Далее с использованием указанных удаленных IP-адреса и порта создается экзем- пляр: IPEndPoint endPoint = new IPEndPoint(remotelPAddress, remotePort); Введенные пользователем строковые данные с помощью класса Encoding. ASCII преобразуются в массив байтов: byte[] bytes = Encoding.ASCII.GetBytes(datagram); Наконец, вызываем метод SendO класса UdpClient и отправляем преобразован- ные байты удаленной конечной точке: sender.Send(bytes, bytes.Length, endPoint);
212 Глава 6 Далее приведен полный код приложения интерактивного форума: using Syst,em; using System.Net; using System.Net.Sockets; using System.Text; using System.Th reading; namespace Wrox Networking.UDP.chatApp { class Chat { > . ... private static IPAddress remotelPAddress; private static int remotePort; private static int localPort; [STAThread] static void Main(string[] args) b try { . ‘ // Получаем данные, необходимые для соединения Console.WriteLine(“Enter Local Port"); localPort = Convert.ToInt16(Console.ReadLine()); Console.WriteLineC“Enter Remote Port”); remotePort = Convert. ToInt16(Console. ReadlineO); Console.WriteLine(“Enter Remote IP address”); remotelPAddress = IPAddress. Pa rse( Console. ReadlineO); // Создаем слушающий поток Thread tRec = new Thread(new ThreadStart(Receiver)); tRec.Start(); while(true) { Send(Console.ReadLineC)); } ) catch (Exception e) { Console.WriteLineC e.ToSt ri ng ()); } 5 private static void Send(string datagram) // Создаем UdpClient UdpClient sender = new UdpClient(); // Создаем IPEndPoint по информации об удаленном хосте IPEndPoint endPoint = new IPEndPoint,. . • (remotelPAddress, remotePort); try I // Преобразуем данные в массив байтов Ж.. byte[] bytes = Encoding.ASCII.GetBytes(datagram),5 * \ // Отправляем данные й sender.Send(bytes, bytes Length, enoPpint) °
UDP 213 J , * . ... . . £ catch .(Exception e) W. .*-^74 g > { <1^*. -^йг '' ’ Console. WriteLine(e.ToString());i" , } finally ,s p. •**’л^’ * 1 < ’ 1 ’& ’?> /// Закрываем; . соединение sender.Closed; '-} V •".. public static void ReceiverO { // Создаем UdpClient для чтения входящих данных UdpClient receivingUdpClient = new UdpClient(localPort); // IPEndPoint с информацией об удаленном хосте IPEndPoint RemotelpEndPoint = null; try л { Console.WriteLine( “—_-..****** *pgady f0J>r chat!! ! ------”); while(true) { , // Ожидание дейтаграммы byte[J receiveBytes - receivingUdpClient Receive( '* ref RemotelpEndPoint); 11 Преобразуем и отображаем данные string returnData = Encoding.ASCII.GetStnng(receiveBytes); Console. WriteLine(“-” + returnData.ToStringO); } } catch (Exception e) Console WriteLine(e.ToStringO); И вот как выглядит результат:
214 Глава 6 Приложение передачи файла Мы узнали, как пользоваться классом UdpCIient для отправки и получения дей- таграмм, и, опираясь на один и тот же принцип, создали приложение интерак- тивной переписки. Теперь научимся пользоваться классом UdpCIient для передачи файла и сериализованного объекта. Программы отправителя и получателя разделе- ны на две логические части. В первой части отправитель посылает получателю (или получателям) информацию о файле (а именно расширение и размер файла) как се- риализованный объект, а во второй части отправляется сам файл. В получателе пер- вая часть принимает сериализованный объект с соответствующей информацией, а вторая часть создает файл на машине получателя. Чтобы сделать приложение бо- лее интересным, откроем сохраненный файл соответствующей программой (на- пример, .doc-файл можно открыть программой Microsoft Word, а . htm-файл — браузером Internet Explorer). Файловый сервер Файловый сервер — это простое консольное приложение, реализованное в классе FileSender. В этом классе есть вложенный класс FileDetails, содержащий информа- цию о файле — тип и размер файла. Начнем с импорта необходимых пространств имен и объявления полей класса. В классе есть пять закрытых полей: экземпляр клас- са FileDetails, объект UdpCIient, а также информация о соединении с удаленным кли- ентом и объект FileStream для считывания файла, который отправляется клиенту: using System; using System.10; using System Net; using System Net. Sockets; using System.Text; . using System.Xml. Serialization;,: using System. Diagnostics; -'4. ’ . ,ц. 1 using System.Threading; public class FileSender „й ... private static FileDetails fileDet new FileDetails(); // Поля,, связанные c UdpCIient .. private static IPAddress remotelPAddress, s private const int remotePort e 5002; private static UdpCIient sender = new UdpClientO; private static IPEndPoint endPoint;
UDP 215 И Объект FileStream private static FileStream fs; Теперь определим класс FileDetails. Чтобы послать через сеть объект FileDe- tails, его требуется сериализовать, поэтому добавляем атрибут [Serializable]. Как уже упоминалось, этот класс содержит лишь два открытых поля: для хранения типа и размера файла: // Информация о файле (требуется для получателя) ife [Serializable] ' public class FileDetails { \ public string FILETYPE = public long FILESIZE = 0; Итак, мы дошли до метода Main() сервера. В этом методе приглашаем пользова- теля ввести удаленный IP-адрес, по которому нужно отправить файл, путь и имя от- правляемого файла. Открываем этот файл в объекте FileStream и определяем его длину. Если она больше максимально допустимой длины, равной 8192 байтам, закрываем UdpClient и FileStream и выходим из приложения. Иначе отправляем информацию о файле, выжидаем две секунды, вызвав метод Thread.Sleep(), и от- правляем сам файл: [STAThread] static void Main(string[] args) ? { try // Получаем удаленный IP-адрес и создаем IPEndPoint Console.WriteLine(“Enter Remote IP address’’); remotelPAddress = IPAddress.Parse(Console.Readtine().ToString()); endPoint = new IPEndPoint(remotelPAddress,remotePort); 11 Получаем путь файла. (Важно: размер файла должен быть меньше 8К) Console.WriteLine(“Enter File path and name to send.”); fs = new FileStream(@Console.ReadLine().ToString(), FileMode.Open, FileAccess.Read); if (fs.Length > 8192) Console.Write(;“This version transfers files with size < 8192 bytes"); sender, Closed; fs. Closed; return; ч > , . SendFilelnfoO; // Отправляем информацию о файле получателю Thread.Sleep(2000); // Ждем 2 секунды > < SendFileO; // Отправляем сам файл natch (Exception е) { Console.WriteLine(e.ToString()) ; Метод SendFilelnfoO заполняет поля объекта FileDetails, а затем сериализует объект в Memorystream, используя объект XmlSerializer. Этот объект считывается
216 Глава 6 в массив байтов и передается методу Send() класса UdpCIient. который отправляет информацию о файле клиенту: public static void SendFilelnfoO { Ц Получаем тип и расширение файла fileDet.FILETYPE = fs.Name.Substring((int)fs.Name.Length - 3, 3): // Получаем длину файла fileDet.FILESIZE = fs.Length; XmlSerializer fileSerializer = new XmlSerializer(typeof(FileDetails)); MemoryStream stream =new MemoryStreamO; // Сериализуем объект fileSerializer.Serialize(stream, fileDet); // Считываем поток в байты stream.Position = 0; byte[] bytes = new byte[stream.Length]; stream.Read(bytes, 0, Convert.Tolnt32(stream. Length)) ; Console. WnteLine( “Sending file details...”); // Отправляем информацию о файле sender.Send(bytes, bytes.Length, endPoint); stream.Close() ; } ’ Метод SendFileO просто считывает содержимое файла из FileStream в массив байтов и отправляет его клиенту: private static void SendFileO { // Создаем файловый поток byte[] bytes = new byteffs.Length]; / / Поток переводим в байты fs.Read(bytes, 0, bytes.Length); Console.WriteLine(“Sending file., size - " + fs,Length + “ bytes”); try sender.Seno(bytes, bytes.Length, endPoint); 7/ Посылаем файл } catch (Exception e) { Console. WriteLine(e .1 oStringO); finally // Чистим объекты fs. CloseO; sender. CloseO; } Console.Read(); .< ... Console.WriteLine(“File, sent succeessfully/’h' r .. .r z ? .............................•• • -«• ' '
UDP 217 Приемник файла Приемник файла — тоже консольное приложение, реализованное в классе FileRecv. Здесь также начинаем с импорта необходимых пространств имен и объяв- ления полей класса: using System; using System.IO, using System.Net; using System.Diagnostics, using System.Net,Sockets; using System.Text; usi ng System.Xml.Serialization; public class FileRecv { private static FileDetails fileDet; A s // Переменные UdpClient private static int localPort = 5002; private static UdpClient receivingUdpClient = new UdpClient(localPort); private static IPEndPoint RemotelpEndPoint = null; private static FileStream fs; private static byte[] receiveBytes = new byte[OJ; Потребуется десериализовать информацию о файле, отправленную сервером, В объект FileDetails, поэтому нужно определить этот класс и в приложении-клиенте тоже: [Serializable] public class FileDetails { public string FILETYPE = public long FILESIZE = 0; } Метод Main() этого приложения только вызывает два метода, чтобы получить, соответственно, информацию о файле и сам файл: [STAThread] static void Main(string!] args) // Получаем информацию о файле GetFileDetailsO ; // Получаем файл ReceiveFileO; } Метод GetFileDetails() вызывает метод Receive() объекта UdpClient. Тот в свою очередь получает от сервера сериализованный объект FileDetails, который сохра- няется в объекте Memorystream. Для десериализации этого потока в объект FileDetails используем объект XmlSerializer и отображаем полученную информа- цию на консоли: private static void GetFileDetails() / 4 try { • - J Console.WriteLine( 4 . ••----***»***Waiting to get File Details •!? *****.♦-——");
218 Глава 6 // Получаем информацию о файле receiveBytes = receivingUdpClient. Receive(ref RemotelpEndPoint); Console WriteLine(“—Received File Detail^"); XmlSerializer fileSerializer = new , XmlSeriaIizer(typeof(FileDetaiis)^; MembryStream streaml = new MemoryStreamQ; /7 Полученные байты - в поток streaml Wnte( receiveBytes 0, receiveBytes.length); streaml.Position - 0, // Обязательно Яа/ // Вызываем метод Deserialize и приводим « Типу объекта fileDet т (ШeDevails)fileSRriaIizer.De.serialize(sTreani1); ConsOJ e.Writeiine (“Received file of type tJ’1 + '4 file, let. FILETYPE !+ ’ whose size is " + fileOet.FILFSIZE.ToString () + “ bytes”); } ’ ' ' catch (Exception e) { - Console Writeline(e.ToStrihgC));** } I .. uc . ’ ' Метод ReceiveFile() получает файл от сервера и сохраняет его на диске под име- нем temp, добавляя расширение, извлеченное из объекта FileDetails. Затем вызываем статический метод Process.StartO и открываем документ связанной с расширением программой: public static void ReceiveFileQ { try 4.;'. { Console .WriteLine! ’—™-*******Waitmg. to- get File!;! *******-——”1; 7/ Получаем файл receiveBytes » receivingUdpClient.Receive!ref RemotelpEndPoint); /7 Поеобразуем и отображаем данные Console WriteLine! “—File received Saving.. ?“h // Создаем файл iemp с полученным расширением fs - new FileStream(“temi.+' fileDet.FILETYPE, ileMode.Create, FileAccess.ReadWrite, FileShare ReadWrite); fs. Write!receiveBytes, Q, receiveBytes length); Console. WriteLine!4*—File Saved... ”); Console.WriteLine C‘--Opening file with associated program- — ,r); Process.Srart(fs Name); /7 Открываем файл -связанной с ним программой catch (Exception е) ! Console.WriteLine(e ToStringO); ) finally { fs.Close();
UDP 219 a receivingUdpClient. CloseO; - vif } - %* 4 Далее показан вывод на консоль на клиенте и сервере, полученный после запус- ка этой программы: Это приложение имеет некоторые ограничения. Во-первых, размер файла зави- сит от размера внутреннего буфера сообщений или предела сети. По умолчанию размер буфера равен 8192 байтам, поэтому невозможно отправлять файлы больше 8192 байтов. Это ограничение преодолимо—достаточно разбить файл на несколько частей, не превышающих размер буфера. Указывая при вызове метода Read() требу- емый размер буфера, можно разделить файл на несколько частей. Протокол UDP не использует сигналов подтверждения, и надо реализовать от- дельный механизм, чтобы перед передачей каждой части проверять, корректно ли принята предыдущая часть. Этот механизм можно создать, если построить в отпра- вителе и получателе еще по экземпляру объекта UdpClient, который будет отслежи- вать подтверждающие сообщения. Широковещательная передача При выполнении этого приложения можно указать отдельный IP-адрес, по кото- рому будет отправлен файл, но также можно задать широковещательный адрес, что- бы отправить файл всем машинам подсети или всей сети. Широковещательный адрес состоит из идентификатора подсети и единиц во всех остальных битах. Например, если хотим разослать сообщение всем хостам в диапазоне 192.168.0, то нужно исполь- зовать широковещательный адрес 192.168.0.255. Чтобы отправить сообщение всем машинам сети независимо от маски подсети, мы можем использовать адрес 255 255.255 255. При широковещательной передаче сообщение отправляется всем ма- шинам сети, а клиенту предстоит решить, хочет ли он обработать эти данные.
220 Глава 6 i Более подробно обсудим широковещательную передачу в следующей главе, когда будем рассматривать групповую рассылку. В отличие от общей рассылки групповые соединения могут пересекать брандмауэр. Они также несильно влияют на пропуск- ную способность и считаются предпочтительнее широковещательной передачи. По- этому в следующей главе сосредоточим внимание на групповой рассылке. Высокоуровневые протоколы, базирующиеся на UDP Протокол UDP полезен там, где порядок доставки не имеет значения и его не нужно обеспечивать. Поскольку отправители} неизвестно, какое назначение актив- но, он использует номер порта, чтобы указать тип обслуживания, требуемый от уда- ленного хоста. В современном мире Интернета существуют многочисленные приложения, ис- пользующие UDP-сервисы, в том числе интернет-телефон, интернет-видеокон- ференции, широковещательное распространение аудиовизуальных данных. Большинство пользователей не знают, что UDP является предпочтительным транс- портным протоколом для DCOM. Не ориентированная на соединения природа UDP позволяет DCOM выполнять определенную оптимизацию, объединяя много- численные низкоуровневые пакеты подтверждений с реальными данными и сооб- щениями тестового опроса (pinging). Этот подход действительно минимизирует число циклов сетевого обмена и, следовательно, повышает скорость и производи- тельность. Сетевые программы компании Microsoft используют UDP для входа в систему, просмотра и разрешения имен. Вот некоторые высокоуровневые протоколы, базирующиеся на UDP: Протокол прикладного уровня Описание/использование RTF (Real-time Protocol) NFS (Network File System) SNMP (Simple Network Management Protocol) RIP (Routing Information Protocol) DNS (Domain Name Service) TFTP (Trivial File Transfer Protocol) RPC (Remote Procedure Call) LDAP (Lightweight Directory Access Protocol) Средства информации реального времени Удаленный файловый сервер Сетевое управление Протокол маршрутизации Разрешение имен хостов Приложение передачи файлов Типичная модель клиент-сервер Служба каталогов Real-time Protocol (RTP) RTP — это протокол прикладного уровня, предназначенный для доставки такой информации реального времени, как аудиовизуальная информация, через частные или общественные сети с одиночными или групповыми IP-адресами. Например, Microsoft NetMeeting использует RTP для рассылки информации реального време- ни через Интернет. Приложение, базирующееся на RTP, реализует RTP, используя протокол UDP и добавляя некоторую дополнительную функциональность, обеспечивающую веде-
221 ние порядковых номеров, идентификацию нагрузки в сети, идентификацию источ- ника и создание меток времени. Network File System (NFS) Еще одним популярным приложением, использующим UDP, является Network File System, которая обеспечивает прозрачный доступ к файлам и файловым систе- мам через сеть. Преимущество NFS перед FTP (File Transfer Protocol) заключено в прозрачном доступе к файлам: NFS может обращаться к части файла, на которую ссылается приложение или процесс. NFS построена на Sun RPC и для выполнения операций с файлами использует за- резервированный порт 2049. Simple Network Management Protocol (SNMP) Как следует из названия, Simple Network Management Protocol (SNMP)—это про- токол управления сетью, широко используемый в сетях. SNMP позволяет админис- траторам наблюдать за удаленными хостами и шлюзами в сети и управлять ими. Служба SNMP может обрабатывать один или несколько запросов от хоста. Этот про- токол организует взаимодействие между программой управления, выполняемой ад- министратором, и агентом сетевого управления, выполняемой на хосте. SNMP использует порты 161 и 162 для менеджера и агента соответственно. Domain Name Service (DNS) Domain Name Service—это распределенная база данных, используемая приложе- ниями TCP/IP для установления соответствия между именами хостов и IP-адреса- ми. Для приложений DNS предпочтителен протокол UDP, использующий порт 53 для отправки запросов DNS на сервер имен. Trivial File Transfer Protocol (TFTP) TFTP—еще один протокол прикладного уровня, полезный для передачи файлов между удаленными хостами. Этот протокол предназначен для использования на са- мозагружаемых бездисковых системах. Для действий при передаче файлов исполь- зуется UDP-порт 69. Итоги В этой главе мы обсудили основы протокола UDP и сравнили его с протоколом TCP. Выяснили, как реализовать протокол UDP в классе UdpCIient платформы .NET. Обсудили члены класса UdpCIient и узнали, как их применять в программах. Затем рассмотрели два более крупных приложения — приложение интерактивного форума, демонстрирующее применение класса UdpCIient для двустороннего взаимодействия, и приложение файлового сервера/получателя, которое может ис- пользоваться для отправки файлов по заданному адресу или может рассылать файл по всем адресам в сети или подсети. Наконец разобрали некоторые высокоуровне- вые, основанные на UDP протоколы для демонстрации тех видов задач, для кото- рых идеально подходит UDP.
ГЛАВА 7 Сокеты групповой рассылки D вероятно, вы помните, как в 1994 г. по Интернету для всех желающих в пря- мом эфире транслировался концерт “Роллинг Стоунз”. Эта возможность появилась благодаря групповой рассылке — той самой технологии, которая позволяет наблю- дать за космонавтами, находящимися на орбите, проводить в Интернете совещания ИТ. д. Для этих приложений однонаправленная передача была бы неуместна, посколь- ку внимание тысяч клиентов к этим мероприятиям привело бы к чрезмерной пере- грузке сервера и сети. Групповая рассылка означает, что сервер лишь один раз отправляет сообщение и оно распространяется для целой группы клиентов. Только системы, входящие в группу, участвуют в сетевом обмене. В нескольких последних главах обсуждалось программирование сокетов с ис- пользованием протоколов, ориентированных на соединения, и протоколов, не требующих организации соединений. В главе 6 было показано, как с помощью про- токола UDP можно передавать широковещательные сообщения. В данной главе речь снова пойдет о протоколе UDP, но теперь обратимся к групповой рассылке. Групповая передача может использоваться для групповых взаимодействий в Интернете, где каждый узел, участвующий в таких взаимодействиях, должен при- соединиться к группе, созданной для этой цели. Маршрутизаторы могут направлять сообщения всем заинтересованным в них узлах. В этой главе создадим два Windows-приложения, использующие средства груп- повой рассылки. Построив первое приложение, можно вести интерактивную переписку с несколькими системами, из которых каждая выступает в роли как от- правителя, так и получателя. Второе приложение представляет собой кинотеатр и демонстрирует, как объемные пакеты данных могут посылаться нескольким кли- ентам, не занимая значительную долю пропускной способности сети. В частности, займемся следующими вопросами: □ Сравним однонаправленные, широковещательные и групповые передачи □ Исследуем архитектуру групповой рассылки □ Реализуем в .NET сокеты групповой рассылки
Сслеты групповой рассылки 223 □ Создадим приложение группового интерактивного форума □ Создадим приложение групповой демонстрации изображений Однонаправленные, широковещательные и групповые передачи Internet Protocol поддерживает три вида IP-адресов: □ Однонаправленные—сетевые пакеты посылаются в один пункт назначения. □ Широковещательные — дейтаграммы отправляются всем узлам подсети. □ Групповые дейтаграммы — отправляются всем узлам, находящимся, быть может, в нескольких подсетях, которые принадлежат одной группе. Протокол TCP обеспечивает ориентированное на соединение взаимодействие, при котором две системы обмениваются между собой, но с этим протоколом можно отправлять только однонаправленные сообщения. Если несколько клиентов соеди- няются с одним сервером, каждый из них поддерживает отдельное соединение с сервером. Серверу требуются ресурсы для всех этих одновременных соединений, и он должен взаимодействовать отдельно с каждым клиентом. Не забывайте, что протокол UDP также можно использовать для посылки однонаправленных сообще- ний и он в отличие от TCP не устанавливает соединения, делая обмен более быс- трым, хотя и не таким надежным, как при использовании TCP. / О посылке однонаправленных сообщений через TCP говорилось в главе 5, а использова- ние UDP для однонаправленной передачи обсуждалось в главе 6. Широковещательный адрес распознается по IP-адресу, в котором все биты хос- та установлены в 1. Например, чтобы отправить сообщения всем хрстам подсети с маской 255.255.255.0 в сети с адресом 192.168.0, нужно задать широковещательный адрес 192.168.0.255. Тогда любой хост с IP-адресом, начинающимся с 192.168.0, будет получать широковещательные сообщения. Широковещательные сообщения всег- да передаются при взаимодействии без организации соединений с использованием протокола UDP. Сервер отправляет данные независимо от того, слушает ли его хоть один клиент. По соображениям производительности было бы невозможно устано- вить отдельное сообщение с каждым отдельным клиентом. Взаимодействие без организации соединений означает, что сервер не должен выделять ресурсы для каждого клиента в отдельности—сколько бы клиентов ни слушали сервер, на нем бу- дут расходоваться одни и те же ресурсы. Разумеется, механизм, не устанавливающий соединения, имеет свои недостат- ки. Например, нет никакой гарантии, что данные получит хоть кто-нибудь. Если хотим обратить внимание на надежность, то придется добавить собственный меха- низм квитирования на уровне более высоком, чем UDP. При широковещательной передаче для любой станции подсети-адресата возни- кает проблема производительности, поскольку каждая станция этой подсети дол- жна проверять, нужен ли ей полученный пакет. Сообщения могут интересовать все станции в сети, и поэтому они поднимаются вверх на каждой станции по стеку про- токолов вплоть до транспортного уровня, где только и можно установить их реле- вантность. С широковещательной передачей существует и другая проблема—они не пересекают границы подсетей. Маршрутизаторы не пропускают широковещатель- ные сообщения, и это правильно, иначе мы быстро бы столкнулись с перенасыще- нием сети. Таким образом, широковещательная передача может распространяться только внутри конкретной подсети.
224 Глава 7 Широковещательная передача полезна, если несколько узлов одной под- сети должны получать информацию одновременно^ Примером полезно- го использования широковещательной передачи служит NTP (Network Time Protocol). Групповые адреса задаются IP-адресами класса D (от 224.0.0.0 до 239.255.255.255). Многопунктовые пакеты могут перемещаться по разным сетям че- рез маршрутизаторы, поэтому можно их применять в сценарии с Интернетом, если провайдер вашей сети поддерживает групповую рассылку. Хосты, которым требует- ся получать определенные групповые сообщения, должны зарегистрироваться че- рез протокол IGMP (Internet Group Management Protocol). Тогда групповые сообщения не отправляются в те сети, гДе нет ни одного хоста, присоединившегося к группе. IP-адреса класса D используются в группах рассылки, чтобы их можно было отличить от обычных адресов хостов, позволяя узлам легко определить, инте- ресует ли их данное сообщение. Модели приложений с групповой рассылкой Имеется много типов приложений, в которых групповая рассылка приносит значительную пользу. В одном из таких сценариев каждой станции в группе нужно, чтобы ее данные отправлялись всем другим станциям (взаимодействие многих со многими). Групповая рассылка означает, что ни одной станции не требуется созда- вать соединение с каждой другой станцией, а вместо этого можно пользоваться групповым адресом. Одноранговое приложение интерактивного форума, как видно из следующей схемы, выиграет от такой схемы. В этом приложении автор сообще- ния передает его в сеть один раз, и оно поступает в каждый узел группы:
Сокеты групповой рассылки 225 Групповая рассылка играет важную роль еще в одном сценарии, в котором од- ной станции требуется отправлять данные группе станций (взаимодействие одного с многими). Это может быть полезно для отправки аудиовизуальных и других объем- ных типов данных. Сервер отправляет данные только один раз по групповому адре- су, а большое число станций может слушать его сообщения. Во время концерта “Роллинг Стоунз” в ноябре 1994 года при помощи групповой рассылки была осуще- ствлена первая попытка передачи в Интернете аудиовизуальных данных прямого эфира рок-концерта. Эта акция имела значительный успех и продемонстрировала полезность групцовой рассылки. Та же технология используется в локальной сети для одновременной установки приложений на сотнях PC, и серверу не требуется от- правлять большой установочный пакет каждой клиентской станции в отдельности: Архитектура сокетов групповой рассылки Групповые сообщения отправляются с использованием протокола UDP группе станций, определенных адресом подсети класса D. Как далее можно увидеть, опре- деленные диапазоны адресов класса D зарезервированы для конкретного использо- вания. Кроме UDP для регистрации клиентов, которые хотят получать сообщения определенной группы, используется Internet Group Management Protocol (IGMP). Этот протокол встроен в IP-модуль, он позволяет клиентам присоединяться к груп- пе и выходить из нее. В данном разделе рассмотрим основные вопросы групцовой рассылки и ряд дру- гих важных факторов: О Протокол IGMP □ Групповые адреса □ Маршрутизация □ Диапазон □ Масштабируемость □ Надежность □ Безопасность
226 ~ Глава 7 Протокол IGMP IGMP используется хостами IP, чтобы сообщать о групповом членстве всем не- посредственно связанным с ними маршрутизаторам, которые способны под- держивать групповую рассылку. Аналогично протоколам ICMP, протокол IGMP реализован в модуле IP, как показано на следующем рисунке. Сообщения IGMP за- ключены в дейтаграммах IP с номером IP-протокола, равным 2. В главе 1 мы позна- комились с номером протокола, который указывается в заголовке IP и равен 2 для IGMP, 1 для ICMP, 6 для TCP и 17 для UDP. Сообщение IGMP версии 2 состоит из 64 битов и содержит тип сообщения, мак- симальное время ответа (используемое только для опросов членства), контрольную сумму и групповой адрес: Тип (8 битов) j Макс* ® Контрольная сумма (16 битов) J-----------------1----------L—i--------------------------------- Групповой адрес (32 бэта) __ _____ _____ ______________________________________ Протокол IGMP версии 2 определен в RFC 2236 (http://wwwJetf.org/rfc/rfc2236. txt). Типы сообщений, используемые для взаимодействия между хостом и маршрути- затором, определены в первых 8 битах заголовков сообщений IGMP версии 2 и опи- саны в следующей таблице: Шестнадца- теричное значение Сообще- ние Описание 0x11 Запрос членства Эти сообщения используются маршрутизатором для определения существования членов группы. Два типа запроса членства можно различить по адресу группы в 32-битном поле группового адреса. При общем запросе адрес группы в заголовке IGMP содержит все нули, тогда запрос касается всех групп, имеющих члены в присоединенной сети. Запрос по конкретной группе требует ответить, есть ли в сети члены конкретной группы. 0x16 Доклад о членстве версии 2 Когда хост присоединяется к группе рассылки, он отправляет маршрутизатору доклад о членстве, информируя маршрутизатор, что входящая в сеть система ожидает групповых сообщений.
Сркеть! групповой рассылки 227 продолжение таблицы 0x17 Выход из Последний оставшийся хост группы, покидая группу, должен группы отправить сообщение о выходе из группы всем маршрутизаторам (224 0 0.2). Хост может запоминать все хосты, входящие в группу (эту информацию он получает в докладах о членстве, отправленных в ответ на запросы о членстве), поэтому он знает, последний ли он в этой группе. Однако это необязательное требование. Если члены группы не запоминаются хостами, то каждый хост, покидая группу, отправляет сообщение о выходе из группы. В любом случае маршрутизатор проверяет, является ли этот хост последним, и, если это так, перестает направлять в сеть групповые сообщения. 0x12 Доклад о Этот доклад используется по соображениям совместимости членстве версии 1 с IGMP версии 1. Версии IGMP В версии 2 протокола IGMP добавлено сообщение выхода из группы, обеспечи- вающее явный выход клиента из группы. В версии 1 был предусмотрен тайм-аут, ко- торый мог длиться до пяти минут. В течение этого времени в сеть отправлялись ненужные сообщения, и в случае значительных по объему данных, например пото- ков аудиовизуальной информации, они могли существенно влиять на доступную пропускную способность. При выходе из группы в соответствии с IGMP версии 2 эта задержка сократилась всего до нескольких секунд. В версии 3 IGMP, находящейся пока в стадии проекта, но уже доступной на Windows ХР, добавлены специальные сообщения о присоединении и выходе с адре- сами источников. Это средство делает возможным протокол Source-Specific Multicast (SSM). В версии 2 IGMP каждый член группы может отправлять групповые сообщения всем остальным членам группы. SSM позволяет ограничить отправителя (источник) в группе одним или несколькими конкретными хостами, что создает значительные преимущества в сценарии приложения “один со многими”. Формат сообщений IGMP версии 3 отличается от сообщений IGMP версии 2, и размер сооб- щения IGMP версии 3 зависит оттого, сколько используется адресов источников. В момент написания этой книги версия 3 IGMP была доступна в варианте проекта. С ее выпуском новый RFC заменит RFC 2236. Групповые адреса Групповой адрес класса D начинается с двоичного значения 1110 в первом бай- те, что составляет диапазон адресов от 224. О. О. О до 239.255.255.255. Однако не каждый адрес из этого диапазона доступен для групповой передачи, например, групповые адреса 224.0 0.0-224.0 0.255 предназначены для специально- го использования, и маршрутизаторы не передают их через сети. Если обычные IP-адреса может назначать местное представительство любой страны, то только Агентство по выделению имен и уникальных параметров протоколов Интернета (IANA, http://www.iana.org) отвечает за назначение групповых адресов. RFC 3171 определяет использование специальных диапазонов групповых IP-адресов и их на- значение. Для краткой записи диапазона IP-адресов в документе RFC 3171 используются адреса бесклассовой междоменной маршрутизации (Classless InterDomain Routing,
228 Глава 7 или CIDR). CIDR-запись 224.0.0/24 аналогична диапазону адресов в десятичной точечной нотации 224.0.0.0-224.0.0.255. В CIDR-записи первая часть показывает фиксированный диапазон в десятичной точечной нотации, за которой следует число фиксированных битов, таким образом, 232/8 - это CIDR-запись диапазона 232.0.0.0-232.255.255.255. В кратком обзоре групповых адресов рассмотрим три основных способа их вы- деления: □ Статический , □ Динамический □ Связанный с областью действия Статические групповые адреса Статические групповые адреса, необходимые глобально, назначаются агентст- вом IANA. Несколько их примеров приведены в следующей таблице: 1Р-адрес Протокол Описание 224.0.0.1 Все станции этой Адреса, начинающиеся с 224.0.0, принадлежат блоку подсети управления локальной сетью (LNCB) и никогда не 224.0.0.2 Все маршрутизаторы этой подсети передаются маршрутизатором дальше Например, адрес 224.0.0.1 означает отправку сообщения всем системам подсети, 224.0.0.2 — отправку сообщения всем маршрутизаторам подсети. Сервер DHCP отвечает блоку 224.0.0.12 Сервер DHCP управления локальной сетью (LNCB) и никогда на сообщения с IP-адресом 224.0.0.12, но только в подсети. 224.0.1.1 NTP, Network Time Адреса в CIDR-диапазоне 224.0.1/24 принадлежат * Protocol межсетевому управляющему блоку (internetwork Control 224 0 1 24 WINS Server Block) Сообщения, отправленные по этим адресам, могу) передаваться маршрутизатором дальше. Например, запросы Network Time Protocol и WINS. Статический адрес, используемый для протоколов, которым нужны известные адреса, представляет всеобщий интерес. Такие адреса могут жестко кодироваться в приложениях и устройствах. Полный перечень зарезервированных в настоящее время групповых адресов и их владельцев, определенных RFC 3171, можно найти на Web-сайте IANA на странице http://www.iana.org/cssignments/multlcast-addresses. Web-сайт IANA предлагает форму, позволяющую запросить групповые адреса дим приложений, которым нужны глобально уникальные IP-ddpeca. Ее можно найти на cmpaH^ehttp://www.iana.org/cgi-bin/multlcast.pl. Динамические групповые адреса Часто динамический групповой адрес будет соответствовать цели лучше, чем фиксированный статический адрес. Эти выделяемые по требованию адреса имеют определенное время жизни. Запросы динамических групповых адресов в принципе аналогичны запросам DHCP (Dynamic Host Configuration Protocol), и действитель- но в первых версиях MADCAP (Multicast Address Dynamic Client Allocation Protocol) базировался на DHCP. Более поздние версии MADCAP абсолютно независимы от DHCP, поскольку к ним предъявляются совершенно иные требования.
Сокеты групповой рассылки 229 Пользуясь протоколом MADCAP, клиент, запрашивающий групповой адрес, от- правляет однонаправленное или групповое сообщение серверу MADCAP. Сервер отвечает адресом, выдавая его в аренду. Протокол MADCAP определен в RFC2730. Сервер MADCAP поставляется с Windows 2000 Server; при помощи конфигурирования его можно сделать частью служб сервера DHCP. Групповые адреса, связанные с областью действия Групповые адреса, связанные с Областью действия, — это такие групповые ад- реса, которые используются только внутри локальной группы или организации. Диапазон адресов от 239.0.0.0 до 239.255.255.255 зарезервирован для админис- тративных адресов, которые могут совместно использоваться несколькими ло- кальными группами. При конфигурировании маршрутизаторы обычно снабжаются фильтрами, не выпускающими трафик групповой рассылки в этом адресном диапа- зоне из локальной сети. Административные адреса, связанные с областью действия, определены в документе RFC2365. Еще один способ определения области действия групповых адресов заключает- ся в использовании TIL. Эту возможность рассмотрим в следующем разделе. Маршрутизация Введение в Интернет возможностей групповой рассылки было непростой зада- чей. Когда протокол групровой рассылки был определен, изготовители маршрути- заторов не сразу реализовали функциональность групповой рассылки, сомневаясь, будет ли это средство использоваться реально. Вместо этого они предпочли вы- ждать, пока не выяснилось, что их потребителям действительно нужна технология групповой рассылки. И тогда оказалось, что реализовать эту технологию немедлен- но нельзя, поскольку ее было невозможно применить в Интернете. Чтобы разре- шить эту дилемму, в 1992 г. была создана магистраль групповой рассылки (МВопе). МВопе начиналась с 40 подсетей в четырех различных странах, а теперь она охваты- вает 8400 подсетей в 25 странах. МВопе соединяет изолированные подсети, способные обмениваться сообщения- ми групповой рассылки через туннели, как можно видеть на следующем рисунке. Чтобы соединить несколько изолированных участков через Интернет, где не поддер- живается групповая рассылка, групповые сообщения направляются дальше с исполь- зованием однонаправленного соединения между конечными точками туннелей:
230 Глава 7 Маршрутизаторы могут выполнять маршрутизацию групповых сообщений, но многие интернет-провайдеры по-прежнему не поддерживают групповую рассылку, поэтому МВопе остается полезным средством. Web-страницу, на которой сообщается о реальном состоянии сетей в Интерне- те, допускающих групповую рассылку, можно найти по URL http://www.multicasttech.com/status. В настоящее время МВопе используется для групповой рассылки аудиовизуаль- ной информации, технических обсуждений и семинаров, данных о полетах косми- ческих челноков NASA и т. д. Инструменты МВопе (например, sdr или multikit) обеспечивают информацией о запланированных мероприятиях с групповой рас- сылкой. Как же клиенту отправляется групповой пакет? На следующем рисунке показан сервер, отправляющий групповые сообщения по конкретному адресу групповой рассылки. Клиент, желающий получать групповые пакеты для определенного адре- са групповой рассылки, должен присоединиться к этой группе. Предположим, кли- ент Е присоединяется к группе, отправив запрос IGMP маршрутизаторам в своей локальной сети. Ко всем маршрутизаторам подсети можно обратиться, используя IP-адрес 224.0.0.2. На рисунке только маршрутизатор Z находится в подсети Е. Мар- шрутизатором регистрируется клиент как член группы рассылки и информирует другие маршрутизаторы, пользуясь другим протоколом, чтобы передавать инфор- мацию о членах группы рассылки через маршрутизаторы. Вкратце обсудим про- токол групповой рассылки, используемый маршрутизаторами: MOSPF, PIM или DVMRP. Маршрутизаторы, оснащенные средством групповой рассылки, передают информацию о члене этой группы другим маршрутизаторам. От сервера требуется лишь отправить UDP-сообщение по групповому адресу. Поскольку маршрутизатор X теперь знает, что есть клиент, желающий получать эти сообщения, он переправ- ляет групповое сообщение маршрутизатору Z, который в свою очередь направляет его в подсеть клиента Е. Клиент Е может считать групповое сообщение. Это сообще- ние не попадет в сеть маршрутизатора Y, поскольку ни один клиент этой сети не присоединился к группе рассылки.
Сокеты групповой рассылки 231 Область действия групповой рассылки определяет, сколько раз групповые сооб- щения переправляются маршрутизаторами. Посмотрим, как можно повлиять на об- ласть действия. Установка области действия Мы уже обсуждали установку области действия, когда рассматривали админис- тративные, зависящие от области действия групповые адреса, которые принадле- жат к конкретному адресному диапазону класса D, назначаемому агентством IANA. Однако, чтобы установить область действия групповых сообщений, можно приме- нить другой способ. Когда клиент отправляет доклад о членстве, желая присоединиться к группе рас- сылки, он определяет значение TTL (время жизни), посылаемое в пакете IP. Значе- ние TTL определяет, сколько “прыжков” должен выполнить доклад о членстве. Значение TTL, равное 1, означает, что этот доклад так и не покинет локальной сети. Каждый маршрутизатор, получив доклад о членстве, уменьшает значение TTL на единицу и отбрасывает пакет, когда значение TTL достигает нулевого значения. При определении точного числа “прыжков” до отправителя возникаег пробле- ма, связанная с тем, что разные маршруты требуют разного числа прыжков, и поэто- му административные групповые адреса, связанные с областью действия, имеют некоторое преимущество. Протоколы маршрутизации Для перенаправления докладов о членстве в группах и поиска наилучшего пути от отправителя до клиента маршрутизаторами могут использоваться разные прото- колы. Когда создавалась магистраль МВопе, использовался единственный прото- кол Distance Vector Multicast Routing Protocol (DVMRP). В настоящее время для групповой рассылки широкое распространение получили другие протоколы: Mul- ticast Open Shortest Path First (MOSPF) и Protocol Independent Multicast (PIM). В протоколе DVMRP применяется алгоритм лавинной маршрутизации по обрат- ным путям, в котором маршрутизатор посылает копию сетевого пакета по всем пу- тям, за исключением того пути, откуда пришел пакет. Если в сети маршрутизатора ни один узел не входит в группу рассылки, то маршрутизатор отправляет отсекаю- щее сообщение обратно маршрутизатору-отправителю, чтобы тот знал, что ему не нужно получать пакеты для этой группы рассылки. Теперь должно быть понятно, почему DVMRP не слишком хорошо масштабируется! Протокол MOSPF представляет собой расширение протокола OSPF (Open Shor- test Path First). При использовании MOSPF все маршрутизаторы должны знать о на- личии связей с сетями, содержащими члены групп рассылки. MOSPF вычисляет маршруты при получении трафика групповой рассылки. Поскольку маршрутизато- ры обмениваются маршрутами MOSPF, используя OSPF, то MOSPF может ис- пользоваться только в сетях, в которых в качестве протокола маршрутизации однонаправленной передачи используется OSPF. Кроме того, MOSPF не будет хоро- шо масштабироваться, если используется много групп рассылки или группы часто изменяются, поскольку в этих случаях будет поглощаться значительная вычисли- тельная мощность маршрутизаторов. В протоколе PIM для отправки сообщений членам группы используются два раз- ных алгоритма. Если члены группы широко разбросаны по нескольким сетям, при- меняется алгоритм PIM-SM (растянутый режим), а если группа использует лишь несколько сетей, применяется алгоритм PIM-DM (плотный режим). В плотном ре- жиме применяется алгоритм лавинной маршрутизации по обратным путям, похо- жий на DVMRP, но может использоваться любой однонаправленный протокол
232 Глава 7 маршрутизации. Режим PIM-SM определяет точку регистрации для надлежащей маршрутизации пакетов. Масштабируемость Огромным преимуществом групповой рассылки является масштабируемость. Если, к примеру, отправить 123 байта тысяче клиентов, используя однонаправлен- ную передачу, сеть будет загружена 123 тысячами байтов, поскольку сообщение дол- жно быть послано по одному разу каждому клиенту. При использовании групповой рассылки для отправки тех же 123 байтов тысяче клиентов требуется послать в сеть лишь 123 байта, и все клиенты получат одно и то же сообщение. С другой стороны, если отправить 123 байта с использованием широковеща- тельной рассылки, загрузка в сети будет ниже, чем при групповой рассылке тех же 123 байтов. Однако широковещательная передача уступает групповой рассылке не только тем, что ее сообщения не могут пересекать несколько сетей. Кроме того, что- бы все другие клиенты, которых не интересует широковещательное сообщение, смогли определить, что ни один сокет не ждет этого сообщения и пакет им не ну- жен, они должны обрабатывать пакет вплоть до транспортного уровня. Групповая рассылка представляет собой самый эффективный и масштабируе- мый способ передачи сообщений нескольким клиентам. Надежность Групповая рассылка IP не дает никакого совместимого протокола, одновремен- но надежного и реализующего механизм управления потоком. С другой стороны, UDP не гарантирует ни доставку сообщений, ни их прибытие в правильном поряд- ке. Во многих сценариях не возникнет проблем, если какие-либо сообщения будут утеряны. Слушая концерт, транслируемый в прямом эфире, пользователи готовы потерять несколько пакетов — и это лучше, чем добросовестная повторная пере- дача, приостанавливающая воспроизведение. Однако высококачественное прило- жение, заблаговременно кэширующее массу данных, вероятно, предпочтет более надежный механизм. Если надо использовать групповую рассылку для одновремен- ной установки приложения на нескольких рабочих станциях, надежный механизм становится основным фактором. Если некоторые сообщения будут потеряны, уста- новленное приложение не заработает или даже сможет причинить ущерб. Если при групповой рассылке требуется гарантированная доставка, нужно, вос- пользовавшись надежным протоколом, добавить специализированное квитиро- вание. Один из способов, позволяющий этого добиться, состоит во включении номеров пакетов и контрольных сумм в данные, отправляемые группе. Если получа- тель по неверной контрольной сумме обнаруживает искаженный пакет или теряет пакет, он передает отправителю данных сообщение NACK, и отправитель может повторно послать недостающий пакет. Заметим, что использование сообщений NACK в случае ошибок гораздо лучше масштабируется, чем альтернативная отправ- ка подтверждающих пакетов на каждый правильно полученный пакет. В Windows ХР сообщения NACK используются именно так, как часть надежной групповой рассылки этой системы, обеспеченной через Message Queuing. Если ис- пользовать Windows ХР (или Windows .NET Server) на клиенте и сервере, не понадо- бится реализовать механизм NACK самостоятельно.
Сокеты групповой рассылки 233 Безопасность Как сочетается групповая рассылка и безопасность? Различаются вопросы безо- пасности групповой рассылки в зависимости от того, работаем ли мы в Интернете или в интрасети и защищаем ли групповую рассылку внутри группы. Брандмауэр действует как защитный шлюз между Интернетом и интрасетью. Предположим, что интернет-провайдер поддерживает магистраль МВопе на сторо- не Интернета и что групповая рассылка разрешена в интрасети. Брандмауэр будет останавливать сообщения групповой рассылки, проходящие из Интернета в интра- сеть и наоборот, и мы должны явно подключить проход через брандмауэр для адре- са групповой рассылки и порта. Относительно безопасного взаимодействия внутри группы, участвующей в рас- сылке, рабочая группа IETF по безопасности групповой рассылки (MSEC) внесла предложение по управлению ключом группы рассылки, которое обещает устано- вить стандартный способ безопасного взаимодействия между авторизованными членами группы. Это предложение не позволит постороннему читать групповые со- общения. К моменту написания этой книги в IETF уже появился проект документа, описывающего безопасное управление ключом группы. Выпуск этого документа за- планирован в декабре 2002 г. Использование сокетов групповой рассылки в .NET Теперь, после того как обсуждены основные принципы и вопросы, связанные с групповой рассылкой, давайте поиграем с поддерживающими ее классами .NET. Вначале рассмотрим код, нужный для отправителя и получателя. Отправитель У приложения-отправителя нет никаких особых задач, которые не были бы об- суждены в предыдущих главах, поэтому для отправки групповых сообщений просто используем класс UdpClient. Единственное различие в том, что теперь нужно ис- пользовать групповой адрес. Объект remoteEP класса IPEndPOint указывает на адрес группы и номер порта, которые будут использоваться группой: IPAddress groupAddress = IPAddress.Parse(,'234.5l6^ir)1;f int remotePort = 7777; . • , v-у' int localPort = 1111; ........ IPEndPoint; remoteEP ₽ new IPEndPoint(groupAddress, remotePorth UdpClient server = new UdpClient(localPort);, < « * .^. server.Send(data, data.Length, remoteEP); Адрес групповой рассылки нужно сделать известным для клиентов, присоединя- ющихся к группе. Сделаем это, используя фиксированный адрес в конфигура- ционном файле, к которому могут получать доступ клиенты. Другой подход — использовать сервер MADCAP, чтобы получать адрес групповой рассылки динами- чески, в этом случае надо как-то сообщить клиенту о динамически назначаемых адресах. Это можно было бы сделать, используя потоковый сокет, с которым соеди- няется клиент. Такой потоковый сокет реализуем позднее, когда дойдем до прило- жения, демонстрирующего изображения.
234 Глава 7 Получатель Клиенты должны присоединиться к группе рассылки. В методе JoinMulticast- Group() класса UdpCIient эта возможность уже реализована. Этот метод устанавлива- ет опции сокета AddMembership и MulticastTimeToLive и отправляет маршрутизатору IGMP-сообщение доклада группы. В первом параметре метода JoinMulticastGroup() указывается IP-адрес группы рассылки, а второй параметр представляет значение TTL (число маршрутизаторов, которые должны пересылать сообщение с докла- дом). UdpCIient UdpCIient = new UdpClientO; UdpCIient.JoinKult±castGroup(groupAddress, 50); Чтобы выйти из группы, вызываем метод UdpCIient. DropMulticastGroup(), прини- мающий параметр, в котором задается тот же самый адрес групповой рассылки, ука- занный в методе JoinMiilticastGroup(): UdpCIient.DropMulticastGroup(groupAddress); Использование класса Socket Вместо использования класса UdpCIient можно напрямую обратиться к классу Socket. Следующий код выполняет практически то же самое, что и класс UdpCIient. Сокет UDP создается конструктором класса Socket, а затем методом SetSocketOp- tion() устанавливаются опции сокета AddMembership и MulticastTimeToLive. Этот метод уже использовался в главе 4, теперь применим его с опциями групповой рас- сылки. Первый передаваемый в метод параметр—это SocketOptionLevel. IP, посколь- ку протокол IGMP реализован в модуле IP. Во втором параметре указывается значение из перечисления SocketOptionName. Значение AddMembership используется, чтобы отправить IGMP-доклад о членстве в группе, a MulticastTimeToLive устанавли- вает число прыжков, за которое нужно переслать доклад о групповой рассылке. Для доклада о членстве в группе также укажем IP-адрес группы рассылки. IP-адрес можно указать через вспомогательный класс MulticastOption: public void SetupMulticastClienttlPAddress groupAddrass, int timeToLive) { « , >• ( Socket socket - new Socket(AddrcssFamiTy.Internetwork, ' SocketType.Ogr&m, PmiocoIType Uop); MulticastOption multicastoption = pew 4u]ticast0ption(groupAddress); socket, SetSocketOpt ion (SocetOptionLevel IP,, SocKetOptiondanic. AddMembei ship, multicastoption); ?• f socKet.SetSocketOption(SocketOptionLevel. IP. : SockstOptionName.MulticastTimeToLive, timeTotive);1 ь Для выхода из группы рассылки нужно вызвать метод SetSocketOpt ion () со значе- нием SocketOptionName.DropMembership: public void SioDMulticastC]lent(IPAddress groupAddress, jnt timeToLive) Socket socket •» new Socket(Addiessrami]у InterNetwork,. ' C $ Socket Type. DgraK Erotoco^TypeWdplf * Multi u-astOprion mU ticastOption * new MulticastOption(groupAOdiess); socket.Se tSGCketOption(SocketOptiontevel,IP, SocketOptionName.DropMembarehip,
Сокеты групповой рассылки 235 multicastOption}; Преимущество класса Socket по сравнению с классом UdpClient заключается в том, что мы имеем большее число опций для групповой рассылки. В дополнение к уже рассмотренным опциям, позволяющим присоединиться к группе или оста- вить группу, в Windows ХР имеется значение перечисления SocketOptionName. Add- Sou rceG roup, с помощью которого можно присоединиться к группе источников, используя SSM. Создание приложения интерактивного форума Теперь можно приступить к разработке полного приложения групповой рас- сылки. Первым учебным приложением станет простое приложение интерак- тивного форума, к которому могут обратиться несколько пользователей, чтобы отправить сообщения всем остальным клиентам. В этом приложении каждая стан- ция действует и как клиент, и как сервер. Каждый пользователь может ввести сооб- щение, отправляемое всем участникам форума. Для приложения форума строится проект Windows-формы с именем Ми1- ticastChat, создающий исполнимый файл с именем MulticastChat.exe. Класс основ- ной формы в этом приложении называется Chat Form. cs. Пользовательский интерфейс Пользовательский интерфейс приложения интерактивной переписки позволя- ет пользователю ввести имя разговора и присоединиться к сетевым взаимодействи- ям, нажав кнопку Start. Нажимая эту кнопку, пользователь присоединяется к группе рассылки, и приложение начинает слушать адрес группы. Сообщения вводятся в текстовом поле под меткой Message и отправляются группе, когда нажимается кнопка Send:
236 Глава? В следующей таблице показаны основные элементы управления формы с их име- нами и значениями свойств по умолчанию: Тип элемента управления Имя Свойства Текстовое поле textName Text = “” Кнопка buttonstart Text = “Start” Кнопка buttonstop Enabled = false - - Text = “Stop" Кнопка > buttonSend Enabled = false - - Text = “Send" Текстовое поле textMessage Multiline = true - - Text = Текстовое поле textMessages Multiline = true - - Readonly = true - - Scrollbars = Vertical - - Text = “" Полоса состояния statusBar - ChatForm — это основной класс приложения, как можно понять по следующему коду. Этот код показывает пространства имен .NET и закрытые поля, используемые всеми методами, которые будут по ходу дела добавляться в этог класс: using System; v? using Svstem.Configuraiion;^ • - w ; usingSyste"!. Collections. Specialized; using System,Net; using System №1 Sockets; ‘ т using System.Text; usxng System.Threading; using System.Windows.Forms, namespace Wrox. Networking; Mult least { public class ChatForm : System.Windows.Forms.Form { private bool done = true; Ц Флаг остановки слушающего потока, private UdpClient client; // Сокет слиента private IPAddress groupAddress;// Групповой адрес расснлчи private int localPort; // Локальный порт для приема сообщений private int remotePort; 17 Удаленный порт для отправки сообщений private int ttl; ’ private. TPEndPoint remoteEP; private UnicodeEncoding encoding =’new. UnicodeEncodingO;:
Сокеты групповой рассылки 237 private string name; // имя пользователя в разговоре private string message; // сообщение для отправки ' //... Параметры конфигурирования Групповые адреса и номера портов должны быть легко конфигурируемыми, поэ- тому создадим XML-файл конфигурирования приложения с именем Multi- castChat.exe.config и следующим содержанием. Этот конфигурационный файл нужно поместить в тот же каталог, где находится исполнимый файл (при запуске из Visual Studio .NET в отладке это будет каталог bin\Debug). <?xml versiori=”1.0" encoding="utf-8" ?> <configuration> ;.... .,r <appSettings> odd key=”GroupAddress" уа1ие="234/5.в; 11” /> odd key=‘* LocalPort” value=”7777“ /> Odd key-’RemotePort” value®”7777” /> Odd key® "TTL” value="32" Z> </appSettings> </configuration> Значение ключа G roupAdd ress должно быть IP-адресом класса D, как следует из об- суждения в начале этой главы. Ключи LocalPort и RemotePort пользуются одними и теми же значениями, чтобы приложение интерактивной переписки было и полу- чателем, и отправителем с одним и тем же номером порта. Чтобы иметь возможность в целях тестирования запустить это приложение дважды на одной системе, нужно скопировать приложение вместе с конфигурационным файлом в два разных каталога. Поскольку два выполняющихся приложения в одной системе не могут слушать один и тот же порт, также придется изменить номера портов для LocalPort и RemotePort в конфигурационном файле на два разных порта. Для каждого приложения нужно, чтобы значение его ключа RemotePort было равно значению ключа LocalPort второго приложения. В этом файле мы установили значение TTL, равное 32. Если вы не хотите, чтобы доклады о членстве переправлялись через маршрутизаторы в среде групповой рас- сылки, измените это значение на 1. Этот конфигурационный файл считывается конструктором класса Chat Form с ис- пользованием класса System.Configuration.Configurationsettings. Если конфигу- рационный файл не существует или имеет неверный формат, то порождается исключение, которое мы перехватываем и отображаем сообщение об ошибке: public ChatFormO { // // Требуется для поддержки дизайнера Windows-форм InitializeComponent(); try { , Т': Ж,. - // Считываем конфигурационный файл приложения NameValueCollectioH configuration - у . ‘ ’’’г'. « <f J tCorniguratLOnSettings.AppSettings, ’ 4Д groupAddress » IPAddress,Parse(configurationr‘GroupAddfess”j); ‘ localPort = int.Parse(configuration[‘,LocalPort”]); remotePort.® inSParse.(cdnfigUration[“fiemotePort,,i)i
238 Глава 7 ttl« int.Parse(configuration[,‘TTL”]); л > catch < MessageBox.Show(t his, V - "Error in application configuration file! *, *trror Multicast Chat”, MessageBoxButtons.OK, ; MessageBoxIcon Error); buttonstart.Enabled = false; } } } } Присоединение к группе, получающей рассылку В обработчике щелчка по кнопке Start считываем имя, введенное в текстовом поле textName, и записываем его в поле name. Далее создаем объект UdpClient и присое- диняемся к группе, получающей рассылку, вызывая метод JoinMulticastGroupO. За- тем создаем новый объект IPEndPoint, ссылающийся на адрес групповой рассылки и удаленный порт; этот объект мы будем использовать в методе Send () для отправки данных группе: private void OnStart(object sender; System.EventArgs e) I name - textName.Text; textName. Readonly s true; ’ try // Присоединяемся к Группе расСылки client - new UdpClient(localPort); 4 £ < client.JoinMulticastGroupO roupAddress, ttl); remoteEP = new IPEndPoint(groupAddress, remotePort); у ' Поскольку класс UdpClient не поддерживает асинхронные операции, в следую- щей секции кода создаем новый поток, который будет получать содбщения, отправ- ленные.по групповому адресу. Чтобы обеспечить асинхронную работу, можно было бы выбрать альтернативный способ—использовать класс сырого сокета. В свойстве потока IsBackground устанавливается значение true, чтобы поток автоматически прекращал работу при выходе из основного потока. После запуска потока отправляем группе представляющее нас сообщение. Для преобразования строки в массив байтов, как того требует метод SendO, вызываем метод UnicodeEncoding.GetBytes(): ft Запускаем поток, получающий сообщения Thread receiver = new Thread(new ThreadStart(Listener)), receiver.IsBackground = true; receiver. StartO; // Отправляем первое сообщение группе byte[] data = encoding.GetBytes(name + ” has joined the chat1*);, client.Send(data, data.Length, remoteEP); Последнее действие, выполняемое в методе OnStartO, должно подключить кнопки Stop и Send и запретить доступ к кнопке Start. Кроме того, напишем обра- ботчик исключения Socket Except ion, которое может возникнуть, если приложение, слушающее один и тот же порт, запущено второй раз:
Сокеты групповой рассылки 239 X buttonstart.Enabled = false; buttonstop Enabled = true; K-... buttonSend.Enabled * true; E catch (SocketException ex) . X »; X*“ ** “> •. л®s- MessageBox.Show(tnis, ex.Message, "Error MulticastChatX. * X ‘ MessageBoxButtons.OK, MessageBoxicon.Error); ’-'X., '?• •> '>’*' >• EE,1-’ ’ " < ' r:',,!T .SVm;-, Hl! i " . 1 V. • “ Получение сообщений, адресованных группе В методе слушающего потока, который был создан раньше, ждем в методе cli- ent. Receive!), пока не поступит сообщение. С помощью класса UnicodeEncoding полу- ченный массив байтов преобразуется в строку. Теперь возвращаемое сообщение нужно отобразить в пользовательском интер- фейсе. При использовании потоков и элементов управления Windows следует обра- щать внимание на один важный момент. В традиционной среде программирования Windows элементы управления Windows можно создавать из разных потоков, но лишь поток, создавший элемент управления, может вызывать на нем методы, поэто- му все функции обработки элемента управления Windows должны вызываться в со- здавшем его потоке. Для Windows-форм та же самая модель отображается на классы Windows-форм .NET. Все методы элементов управления Windows-форм должны вы- зываться на создающем потоке, за исключением метода Invoke() и его асинхронных версий Beginlnvoke() и Endlnvoke(). Эти методы можно вызывать из любого потока, поскольку они переадресуют вызываемый метод потоку, создавшему элемент управ- ления Windows, а уже тот поток вызывает метод. Поэтому вместо того, чтобы непосредственно отобразить сообщение в тексто- вом поле, вызываем метод Invoke() класса Form, направляющий вызов создающему потоку класса Form. Поскольку этот же самый поток создал текстовое поле, он удов- летворяет всем требованиям. Метод Invoke!) требует параметра типа Delegate, и поскольку любой делегат по- рожден этим классом, то методу может быть передан любой делегат. Мы хотим вы- звать метод DisplayReceivedMessage!), не принимающий параметров, а в среде .NET Framework уже определен делегат для вызова метода без параметров: System. Windows. Forms. Methodlnvoke г. Этот делегат принимает такие методы без параметров, как метод DisplayReceivedMessage!). // Основной метод слушающего потока, который получает данные private void Listener!) < { done « false; 4 try { while (!done) r { IPEndPoint ep = null; v byte[] buffer = client.Receive(ref ep); message = encoding.GetString(buffer); this.Invoke(new Methodlnvpker (DisplayReceivedMessage)); , catch (Exception ex). J -
240 Глава 7 { MessageBox.Show(this, ex.Message, "Error MulticastChat", MessageBoxButtons, OK, MessageBoxIcon.Error)• } В реализации DisplayReceivedMessageO запишем полученное сообщение в тек- стовое поле textMessages и поместим некоторую информацию в полосу состояния: private void DisplayReceivedMessageO string time = DateTime.Now.ToString(“t”); textMessages.Text = time + “ ” + message + “\r\n” + textMessages.Text; statusBar.Text = “Received last message at ” + time; Отправка групповых сообщений Следующая задача состоит в реализации функциональной возможности отправ- ки сообщения в обработчике щелчка по кнопке Send. Как уже было показано, стро- ка преобразуется в массив байтов с помощью класса UnicodeEncoding: private void QnSend(object sender, System. EventArgs e) try { ' // Отправляем сообщение группе byte[1 data » encoding GetBytes(name + “: ” + textMessage.Text); Client.Send(data, data.Length, remoteEP); textMessage.Clear(); textMessage.Focus(); } catch (Exception ex) { MessageBox.Show(this, ex.Message, “Error MulticastChat”, MessageBoxButtons.OK, MessageBoxIcon.Error); } } Прекращение членства в группе Обработчик события щелчка по кнопке Stop, метод OnStop(), останавливает ожи- дание клиентом сообщений групповой рассылки, вызывая метод DropMul- ticastGroup(). Прежде чем клиент прекращает получать данные1 групповой рассылки, группе отправляется заключительное сообщение, информирующее о вы- ходе пользователя из форума: private void OnStop(object sender, System.EventArgs el? ? StopListener(); private void StopListener() { • k _ /й // Отправляем группе сообщение о выходе -W j byte[] data — encoding..GetBytes(name * * has left the chat*’); client,Send(data, data.Length, remoteEP); // Покидаем группу л « client.DropMulticastGroup(grdupAddress);
Сокеты групповой рассылки 241 <> client. Close И Останавливаем поток, получающий сообщения dories true; buttonstart.Enabled = true; buttonstop.Enabled = false; ‘, buttonSend.Enabled = false; } Поскольку членство в группе надо прекратить в любом случае — не только когда пользователь щелкает по кнопке Stop, но и когда он завершает приложение, — обра- ботаем событие Closing для формы в методе OnClosing(). Если к этому моменту еще не остановлен приемный сервер, нужно опять вызвать метод StopListener() и тем самым после отправки группе заключительного сообщения прекратить слушать группу: private void OnClosing(object sender, System ComponentModel.CancelEventArgs e) if (Idone) StopListenerO; } Запуск приложения интерактивного форума Теперь можно запустить приложение интерактивного форума на нескольких станциях и начать переписку. Заметьте, что последние сообщения появляются сверху панели переписки: Приложение демонстрации изображений Затем рассмотрим приложение демонстрации изображений. В этом приложе- нии иллюстрируется сценарий групповой рассылки, в котором одно приложение
242 Глава? отправляет сообщения нескольким клиентам. Для создания этого приложения тре- буется приложить больше усилий, поскольку рассылаемые сообщения имеют более значительные размеры и необязательно могут помещаться в дейтаграммах пакетов. Сервер демонстрации изображений позволяет выбирать в его файловой систе- ме изображения и рассылать их всем клиентам, присоединившимся к группе, полу- чающей рассылку. Демонстрация изображений - это объемное приложение, и поэтому вместо того, чтобы пытаться объяснить его работу полностью, сосредоточимся только на самых важных аспектах, связанных с сетевыми взаимодействиями. Полное работающее приложение можно скопировать с Web-сайта Wrox. Реализация демонстрации изображений Полное приложение демонстрации изображений состоит из трех проектов Visual Studio .NET: □ Pict и reShowSe rve r—Windows-приложение сервера □ PictureShowClient - Windows-приложение клиента □ Pictu rePackage r — библиотека классов, используемая клиентом и сервером Для запуска приложения следует скопировать на сервер исполняемый файл PictureShowServer.ехе и библиотеку PicturePackager. dll. Для клиентских систем тре- буются файлы PictureShowClient. ехе, PicturePackager. dll и конфигурационный файл приложения Pictu reShowClient. ехе. config. До выполнения клиентских приложений следует запустить и инициализировать серверное приложение. Вы должны также установить в клиентском приложении имя вашего сервера. В первую очередь рассмотрим, как отправлять изображения пользователям при- ложения. Создание протокола для изображений Рассылаемые членам группы изображения могут иметь слишком большие разме- ры, чтобы целиком помещаться в одном пакете дейтаграммы. Надо разбить изобра- жение на пакеты. Кроме того, если вместе с потоком изображения отправляются членам группы некоторые другие данные, нужно разработать формат данных, посы- лаемых по сети. С целью упрощения разбора данных, чтобы формат можно было легко расши- рить в будущих версиях приложения, мы используем для пакетов, содержащих данные изображения, формат XML и используем классы из пространства имен Sys- tem.Xml.
Сокеты групповой рассылки 243 Для экономии места можно было бы определить для пакета данных специализированный двоичный формат, но при отправке изображения издержки от обработки XML невелики по сравнению с размером изображения. С XML получаем преимущество от использования существующего анализатора, к тому же этот формат позволит легко добавлять новые элементы в будущих версиях. Один из наших XML-пакетов будет помещаться в одной IP-дейтаграмме. Корне- вым является элемент <PicturePackage>, его атрибут Number идентифицирует фраг- менты, составляющие единое целое. Элементы <Name> и <Data> порождены от элемента <PicturePackage>. <Name> — информационный элемент, используемый для названия изображения; элемент <Data> представляет собой двоичные данные сег- мента изображения в кодировке base-64. Атрибут SegmentNumber позволяет оп- ределить, как объединить сегменты, чтобы создать завершенное изображение, а атрибут LastSegmentNumbe г информирует, сколько сегментов для этого потребуется: < Picture Package Number=’'4"> <Name>hello. jpg</Namei> . <Data SegmentNumber*”2" LastSegmentNumber="l2" Size*"2400"> <!- base-64 encoded picture data -> x/uaia^ </PicturePackage> ’"Ж' . В сборке PicturePackager содержится два класса: класс утилит PicturePackager и класс сущности PicturePackage. Один объект PicturePackage соответствует пред- ставленному выше XML-файлу. Мы можем сгенерировать XML-представление одно- го сегмента изображения, используя метод GetXml() этого класса. Класс PicturePackage представляет два конструктора. Первый из них использует- ся на сервере — он создает фрагмент изображения из простых типов данных (int, string и byte[ ]). Второй конструктор, предназначенный для использования на кли- енте, создает фрагмент по XML-источнику. PicturePackager — это класс утилит, имеющий только статические методы. Он разбивает законченное изображение на несколько сегментов методом GetPic- turePackages() и воссоздает законченное изображение, объединяя составляющие его сегменты методом GetPicture(). Пакеты изображений Итак, класс PicturePackage представляет один сегмент законченного изображе- ния, и здесь воспроизводим его полностью. Сначала определим доступные только на чтение свойства, устанавливающие соответствие с XML-элементами приведен- ного выше формата: using System; using System.Xml; using System.Text; namespace Wrox.Networking.Multicast { public class PicturePackage { private string name; , -4 ",l~ - private int id; , * x .4 < . . >. - Ж. private int segmentNumber; private int numberOfSegments, private byte[] segmentBuffer; Л public -.string'Naeip.^wr,."-'.
244 Глава 7 { get { return name, } } public int Id { get { return id; ‘ > tfe. > public int SegmentNumber * { get { return segmentNumber; } } public int NumberOfSegments get { . ' ' : ? return numberOfSegments; } public byte[] SegmentBuffer { - ,.s get { return segmentBuffer; } ) Далее переходим к созданию двух конструкторов. Один принимает несколько аргументов, позволяя отправителю создать объект PicturePackage, а второй прини- мает полученные из сети XML-данные, чтобы воссоздать объект для получателя: // Конструктор создает сегмент изображения из типов данных // Используется серверным приложением public PicturePackagefstring name int id, int segmentNumber, ....... int numberOfSegments, bytef] segmentBuffer) this.name = name; this, id = id; this.segmentNumber = segmentNumber; this.numberOfSegments * numberOfSegments; this.segmentBuffer = segmentBuffer; // Создает сегмент изображения по XML-коду // Используется клиентским приложением public PicturePackage(XmlDocument xml) XmlNode rootNooe - xml.SelectSingleNodeC'PicturePackage”); id « int.Parse(rootNode.Attributesf"Number”].Value);
Сокеты групповой рассылки 245 XmlNode nodeName = rootNode.SelectSingleNode(“Name"), «• ; this.name у nodeName. innerXml; ,.л " < '''i * 3 1 . J. * s'. '> " 1 XmlNode nodeData - rootNode.SelectSingleNode("Data”); numberOfSegments = int.Parse(nodeData.Attributes! , .. v '' й ”LastSegmentNumber’‘].Valtie); . 7 segmentNumber •= int, Pa r se(nodeData. Attributes! ‘’SegmentNumber ” ] Value); int size '* int.Parse(nodeData,Attributes[“Size”].Value); segmentBuffer « Convert.FromBase64String(nodeData.InnerText); г >x'5, -< v .*> « > '"£' ?. Еще один элемент этого класса—метод GetXmlO, который с помощью классов из пространства имен System.Xml преобразует сегмент изображения в объект XmlDocument. XML-представление возвращается в строке: // Возвращаем XML-код, представляющий сегмент изображения public string GetXmlO • > . - -= { XmlDocument doc » new XmlDocumentO; // Корневой элемент <PicturePacKage> XmlElement picturePackage = doc.CreateElement(“PicturePackage”); // <PicturePackage Number="number''X/PicturePackage> XmlAttribute pictureNumber « doc.CreateAttribute(“Number”); plotureNumber.Value = id.ToStringO; picturePackage.Attribu tes.AppendvpictureNumber); // <Name>pictureName</Name> * * ,, XmlElement pictureName = doc.GreateElement("Name”). pictureName.InnerText name; picturePackage.AppendChiId(pictureName); Э- // <Data SegmentNumber=‘” Size="”> (фрагмент в кодировке base-64) XmlElement data = doc.CreateElement(“Data”); XmlAttribute numberAttr « doc.GreateAttribute("SegmentNumber"); numberAttr.Value = segment Number ToStringO; data.Att ributes- Append(numberAtt r) ; XmlAttribute lastNumberAttr = ’ doc.CreateAttribute(“LastSegmentNumber’’); lastNumberAttr. value = numberOf Segments. ToStringO; data.Attributes.Append(lastNumberAttr); data.InnerText = Convert.ToBase64String($egmentBuffer); ; XmlAttribute sizeAttr » doc.CreateAttributef"Size”); sizeAttr Value = segmentBuffer.Length.ToStringO; data.Attributes Append(sizeAttr); picturePackage.AppendChiId(data); doc.AppendChild(picturePackage); return doc.InnerXml;
246 Глава 7 Более подробную информацию о языке XML и работающих с ним классах .NET можно найти в книге издательства Wrox Professional XML for .NET Developers (ISBN 1-86100-531-8). Упаковщик изображений Класс Pict и rePackage г—это класс утилит, состоящий только из статических мето- дов. Он используется как отправителем, так и получателем. Метод GetPic- tu rePackages() разбивает изображение на несколько пакетов в форме массива объектов Pictu rePackage, в котором каждый сегмент изображения представляется одним объектом PicturePackager using System; using System.Drawing; using System.Drawing.Imaging; using System.10; namespace Wrox Networking.Multicast { public class PicturePackager { protected PicturePackagerO { } // По завершенному изображению создает сегменты изображения public static PicturePackage[] GetPicturePackages(string name, int id, = wage picture J return GetPicturePackagesCname, id, picture, 4000); // Возвращает сегменты изображении для завершенного изображения public static PicturePackagefJ GetPiQturePackages(string-name, int id, • te- image pictuj e;i in<:Segm^tSLz3) //Сохраняем лзпйраженче в массива ба '"оь г MemoryStream stream,; * new MemoryStream()V picture.Savefstream, ImageFormat.Jpeg); // Выделяем число се. тентов, на которые разбивается Изображение int number Segment? = (int)stt^ Position / segmentsize + 1; PicturePackager] packages = new PictureFackagefnumberSegments]; 7 создаем сегменты изображения ~ int souTcelndex » 0; * for (int 1-0; 1 < numbersegments; i++) // Вычисляем размер буфера сегмента . int bytes ЮСору = (lot) st ream. Position - sourceindex; if (bytes7oCopyJ>: segmentSize ) * ’ bytesToCop-, | segmentsize; bytef] segmentBuffer = new byte[bytes7ocopy]; Array.Copy(stream. GetBuffe r(), sourceindex, segmentButfer,
Сокетыгрупповой рассылки 247 О, bytesToCopy); packages[i] = new PicturePackage(name, id, i + 1, • numbersegments, segmentBuffer); sourceindex += bytesToCopy; ' ,Д '”'«£& • i • /'!&*» f! л Г . *| : y , - ’sjg'jj**, ''• '-’•'ч return packages: Получатель использует обратный метод GetPictureO, принимающий все объек- ты PicturePackage для одного изображения, и возвращает завершенный объект изо- бражения: // Метод возвращает завершенное изображение из переданных ему cei ментов public static Image GetPicture(PicturePackage[] packages) { ’ у ч - int fullSizeNeeded = 0; int numberPackages = раскцдез[О]. NumberOf Segments; int pictureid = packagesfO].Id; // Вычисляем размер данных изображения и проверяем согласованность И идентификаторов изображения for (int i=0;*i < numberPackages; 1++) fullSizeNeeded +* packages!?!].SegmentBuffer.Length; if (packages(i).Id !=? pictureid) throw new ArgumentException( "Inconsistent picture ids passed”, "packages”); // Объединяем сегменты в двоичный массив byte[] buffer new byte(fullSizeNeeded]; int destinationindex = 0; for (int 1 => 0; !>< numberPackages; i++) {, ft int length = packages!?!],.SegmentBuffer. Length; Array. Copy(packages!?!]. SegmentBuffer, 0, buffer, destinationindex, length); destinationindex += length;1 j // Создаем объект image из двоичных данных Memorystream stream = new MemoryStream(buffer); Image image = Image.FromStream(stream); return image; » } } } Сборка PicturePackager должна содержать ссылки на сборки System Drawing и System. Xml. Сервер демонстрации изображений Получив сборку, которая разбивает и объединяет изображения, можно при- ступить к реализации серверного приложения. Проект для сервера называется Pic-
248 Глава 7 tureShowServeг, класс его основной формы PictureServe rForm содержится в файле PictиreShowServeг. cs и представляет следующую форму: Элементы управления этого диалогового окна перечислены в следующей таб- лице: I Тип элемента Имя Комментарии Основное меню mainMenu В этом меню содержатся основные элементы File. Configure и Help В меню File есть подменю Init, Start. Stop и Exit. Меню Configure позволяет конфигурировать Multicast Session, Show Timings и Pictures. Меню Help содержит только пункт About. Кнопка buttonPictures Кнопка Pictures... даст возможность пользователю конфигурировать изображения, выбранные для презентации. Кнопка buttonlnit Кнопка Init, использующая сокет TCP, будет публиковать для клиентов групповой адрес и номер порта. Кнопка buttonstart Кнопка Start начинает отправку изображений по групповому адресу рассылки. Кнопка buttonstop Кнопка Stop останавливает демонстрацию изображений до завершения демонстрации. Панель изображения pictureBox В панели изображения показывается изображение, передаваемое в настоящий момент клиентам. Индикатор progressBar Индикатор выполнения показывает, сколько изображений было передано. Полоса состояния statusBar На полосе состояния показывается, что происходит в текущий момент. Список изображений imageList Список изображений будет содержать все изображения, включенные в демонстрацию.
Сокеты групповой рассылки 249 Класс PictureServerForm содержит метод Main(), еще три класса для диалоговых окон и класс InfoServer. Остальные три диалоговых класса, ConfigurePicturesDialog, MulticastConfi- gurationDialog и ConfigureShowDialog, используются для конфигурирования парамет- ров настройки приложения. Диалоговое окно ConfigurePicturesDialog позволяет выбрать в файловой системе сервера файлы’изображений, составляющие демон- страцию. В диалоговом окне MulticastConfigurationDialog устанавливаются адрес групповой рассылки, номер порта и интерфейс, через который должны посылаться изображения, в том случае, если в серверной системе есть несколько сетевых плат. Диалоговое окно ConfigureShowDialog позволяет указать время задержки между изо- бражениями. Еще один класс, InfoServer. cs, запускает собственный поток, который действует как сервер, отвечающий клиентским приложениям. Этот поток возвращает клиен- там адрес группы и номер порта. Рассмотрим код стартующего класса этого приложения, который называется PictureServerForm и находится в исходном файле PictureShowServer. cs. Сначала ис- следуем пространства имен и поля, необходимые классу для методов, которые будут обсуждаться в следующих разделах: using System; using System.Drawing; using System.Windows.Forms; using System.Net; using System.Net.Sockets; using System.Text; using System.10; using System.Xml; , t using System.Threading; namespace Wrox.Networking. Multicast { public class PictureServerForm : System.Windows.Forms.Form { private string[] fileNames; /./ Массив имен изображений private object filesLock = new objectO; // Замок для синхронизации =• *• -s ’ *• // доступа к именам файлов . ' ; . ' " Л private UnicodeEncoding encoding = new UnicodeEncoding ();' s/ % •: • ' • . ' / 5 // Адрес групповой рассылки,, порт и конечная точка private iPAddress groupAddress = IPAddress.Parse(“231.4 5.11"); ' v ’ . У private int groupPort = 8765; 4 private IPEndPoint groupEP; J ; 4 private UdpCIient UdpCIient; private Thread senderThread; // Поток, отправляющий изображения private Image currentImage; // Текущее отправленное изображение private int picturelntervalSeconds = 3; 11 Время между отправкой // изображений //.. • Открытие файлов Одно из первых действий, которые нужно выполнить после запуска сервера приложения, состоит в конфигурировании изображений, представляемых на де-
250 Глава 7 монстрации. Метод OnConf igurePictures() — это обработчик пункта меню Configure I Pictures и щелчка по кнопке Pictures:: private void OnConfigurePictures(object sendee System EventArgs e) . '/'v; { ,15 ‘ »' z ’ ч?; «vi; >. • • - -% ConfigurePicturesDialog dialog = new ConfigurePicturesDialogO; if (dialog.ShowDialog(У == DialogResult OK) 4 { ^. л Z •' £ lock (filesLock) { fileNames « dialog.FileNames; progressBar. Maximum = fileNames. Length; } J Й? J л t. jHf } В этом методе открывается диалоговое окно конфигурирования изображений, которое позволяет предварительно просмотреть изображения в представлении Largelcon элемента Listview: Элементы управления, используемые в этой форме, перечислены в следующей таблице: Тип элемента Имя Комментарии Кнопка buttonSelect Кнопка Select Pictures., отображает диалоговое Диалоговое окно открытия файлов openFileDialog окно openFileDialog, в котором пользователь может выбрать изображения для демонстрации. Кнопка buttonClear Кнопка Clear удаляет все выбранные изображения Кнопка buttonOK Кнопки ОК и Cancel закрывают диалоговое окно Кнопка buttoncancel Если щелчок был сделан по кнопке ОК, выбранные файлы отправляются в основную форму, если по кнопке Cancel, выбор всех файлов игнорируется.
Сокеты групповой рассылки 251 продолжение таблицы Список imageList В компоненте Windows-форм ImageList собираются изображений все выбранные изображения, чтобы отобразить их в списке UstViewPictures для предварительного Списковый элемент управления ListView UstViewPictures просмотра пользователем. Метод OnFileOpen() — это обработчик щелчка по кнопке Select Pictures... Именно здесь из файлов, выбранных в диалоговом окне OpenFileDialog, мы создаем объекты Image и добавляем их в список изображений, связанный с ListView: private void OnFileOpen(object sender, System.EventArgs e) if (openFileDialog.ShowDialogO == DialogResult.DK) fileNames = openFileDialog.FileNames; int imageindex = 0; - foreach (string fileName in fileNames) using (Image image = Image.FromFile(filename)) i - ... ' imageList.Images.Add(image); sr у &' * Ж’.у? ? .** л v s*-. UstViewPictures, Items Add(fi leName, imagelndex++); } Конфигурирование групповой рассылки Еще одно конфигурационное диалоговое окно серверного приложения позво- ляет конфигурировать адрес групповой рассылки и номер порта: В,’."" Диалоговое окно позволяет выбрать локальный интерфейс, который должен ис- пользоваться для отправки сообщений групповой рассылки. Полезно, если в сис- теме есть несколько сетевых плат или она подсоединена как к коммутируемой, так и к локальной сети. Комбинированный элемент-заполняется списком локальных интерфейсов при создании формы. Сначала вызываем метод Dns. GetHostName(), чтобы получить имя
252 Глава 7 локального хоста, а затем метод Dns. GetHostByName(), чтобы получить объект IPHost- Ent гу, содержащий все IP-адреса локального хоста. Если у хоста есть несколько IP-ад- ресов, то в комбинированный элемент добавляется строка “Any”, что позволяет пользователю отправлять групповые сообщения по всем сетевым интерфейсам. Если имеется лишь один сетевой интерфейс, то, поскольку выбрать другой интер- фейс невозможно, комбинированный элемент блокируется. Далее приводится конструктор из файла MulticastConfigurationDialog.cs public MulticastConfigurationDialog() { И I/ Требуется для поддержки дизайнера Windows-форм // InitializeComponent(); — string? hostname = Dns,GetHostName(); '> IPHostEntry entry = Dns.GetHostByNameChostname); IPAddress!] addresses = entry.AddressList; foreach (IPAddress address in addresses) comboBoxLocallnterface.Items.Add(address,TdSt ring()); comboBoxLocallnterface.Selectedlndex - 0; .if {addresses.Length > 1) { ' ' comboBoxLocallnterface.Items.Add(“Any”) ; '! .else • ~ t. 1<•-. - < -- comboBoxLocallnterface.Enabled = false; Еще один интересный аспект этого класса состоит в проверке допустимости ад- реса групповой рассылки. С текстовым полем textBoxIPAddress связан обработчик OnValidateMulticastAddressO, назначенный событию Validating. Этот обработчик проверяет, что введенный IP-адрес находится в допустимом диапазоне групповых адресов: private void OnValidateMulticastAddress(object sender, System.ComponentModel.CancelEventArgs e) ' " try IPAddress address = IPAddress.Parse(textBoxIPAddress.Text); string[] segments = textBoxIPAddress.Text.SplitC / ); int network = int.Parse(segments[O]); fl Проверяем, находится ли адрес в правильном диапазоне if ({network < 224) || (network > 239)) throw new FormatException(“Multicast addresses have the” + “range 224.x.x x to 239.x.x.xr); // Проверяем, что адрес не принадлежит зарезервированному классу D if ((network == 224) && (int.Parse(segments[1]) == 0) && (int.Parse(segments!2]) == 0))
Сокеты групповой рассылки 253 " *\ ' throw new FormatException(“The LocalNetwork Control Block" + \ 4 - J Л, k ‘’cannot be used for multicasting groups”); "4 ' * ». « > catch (FormatExceptijn ex) W ,.A A MessageBox^Show(ex»><essagel;tl WA 'y;„ • - - e. Cancel ^/true; ’«*.>•' « --.A Л - } ;• > ‘ Wit -H. Теперь вернемся к событиям в основном классе PictureServerForm из файла Pic- tu reShowSe rve г. cs. Когда пользователь щелкает по кнопке Init основного диалогового окна, нужно запустить слушающий сервер, чтобы он мог отправлять групповой ад- рес и номер порта присылающим запросы клиентам. Для этого создаем объект InfoServer, задав для него IP-адрес и номер порта, а затем вызываем метод Start(). Этот метод начинает новый поток, обрабатывающий запросы клиентов, как увидим дат ьше. В завершение обработчик On Init () вызывает вспомогательные методы, что- бы сделать доступной кнопку Start и отключить кнопку Init: //Pictur.eShnwServGr cs A >« ; private /old Onlnit(object sender, System EventArgs e) InfoServer info ® new InfoSeryerCgroupAddress^ groupPort); ; info.Start(); /Л . Л v . Л ^'4/^ UIEnableStart(true); UIEnableInit(fal se), “ A } W’SW * ?- W --v*- Класс InfoServe г выполняет всю работу, необходимую, чтобы на отдельном пото- ке получать запросы клиентов и отвечать им адресом групповой рассылки и номе- ров порта. Конструктор класса инициализирует объект InfoServer групповым адресом и номером порта. Метод Start() (вызываемый обработчиком OnlnitO в классе PictureShowServeг) создает новый поток. Основной метод нового потока, InfoMain(), создает потоковый сокет TCP, куда просто помещаем адрес групповой рассылки и номер порта, разделив их символом двоеточия (:), чтобы отправить клиенту сразу же, как он соединится с сервером: // InfoServer. cs using System; using System.Net; using System.Net Sockets; ; using System.!ext; using System.Threading; namespace Wrox.Networking.Multicast { public class InfoServer { private JPAdaress groupAddress; private int groupPort; private UnicodeEficoding encoding - new tlnlcodeEncodingC); public InfoServerdPAddress groupAdqres" int grgupPortl г this.groupAddress » groupAddress^ ; this..groupPort « greupPort; < r
254 Глава 7 } • . ,l V? public void Start0 { // Создаем новый слушающий поток ?_• . Thread infoThread = new Thread(new ThreadStart(lnfoMain)); infoThread.IsBackground = true; infoThread.Start(); protected void InfoMainO { string configuration = groupAddress Address.ToString() + + groupPort.ToStringO; // Создаем потоковый сокет TCP, слушающий запррсы клиентов Socket infoSocket = new Socket(Addres,sFamiiy.InterNetwork, SocketType.Stream, v ‘f ProtocolType.Tcp); try { . .. . infoSocket.8ind(new IPEndPoint(.IPAddress.Any, 8777)); infoSocket. Listen(§); while (true) { Lt Отправляем клиентам информацию о конфигурации Групповой рассылки Socket clientconnection = infoSocket.. Accept (); clientconnection.Send(encoding.GetBytes(configuration)); clientConneotion. Shutdown( Socketshutdown. Both); clientconnection. ClbseO; (1 finally 1 I '. infoSocket. Shutdown (Socketshutdown. Both); * - • & infoSocket. Closed; .-n .->• - -v ) } В главе 4 потоковые сокеты были исследованы более подробно. Вспомогательный метод UlEnableStartO разблокирует или блокирует кнопку Start, чтобы помешать пользователю щелкнуть не по той кнопке. Этот метод вызы- вается з методе OnInit (), и он очень похож на UIEnableInit(): Ц PictureShowServertcs - 1 private void UIEnableStart(bool flag) If (flag) 5 : - { buttonstart.Enabled = true; buttonstart.BackColor = Color SpringGreen; , miFileStart Enabled = true; else {
Сокеты групповой рассылки 255 buttonstart.Enabled = false; buttonstart.BackColor = Color.LightGray; miFileStart. Enabled = false; Л. } •' J->'" t7- } Отправка изображений В методе OnStartO обрабатывается событие щелчка по кнопке Start, здесь же инициализируется и запускается отправляющий изображения поток. Метод, запус- кающий этот поток, называется SendPictures(), и его мы теперь рассмотрим. Z/ Pictu reShowSe rve г. cs private void OnStart(object sender, System.EventArgs e) { ’/ ' ? if (fileNames == null) { MessageBox.Show("Select pictures before starting the show!"'); return; } // Инициализируем поток, отправляющий изображения senderThread = new Thread(new ThreadStart(SendPictures)); senderTh read. Name s "Sender"; senderThread.Prlprity = ThreadPriority.BelowNormal; senderThread.StartO; * > UIEnableStart(false); UIEnableStop(true); ) Список имен файлов, полученный в диалоговом окне Configure Pictures, исполь- зуется методом SendPictures(), который отправляет изображения группе. Здесь за- гружаем файл, имя которого содержится в массиве fileNames, создаем новый объект Image и передаем его методу SendPicture(). Затем с помощью делегата обновляется элемент индикатора, чтобы он мог отражать идущий процесс. Создавая приложе- ние группового интерактивного форума, мы обсуждали вопросы, связанные с ис- пользованием элементов управления Windows в нескольких потоках, и теперь снова приходится обратиться к методу Invoke(): // Pictu reShowSe rve г.cs private void SendPicturesO > ~ { InitializeNetwork(); lock (filesLock) { int pictureNumber = 1; foreach (string fileName in fileNames) { currentimage = Image.FromFile(fileName); Invoke(new Methodlnvoker(SetPictureBoxImage)); SendPiptureCcurrentlmage. fileName, pictureNumber); Invoke(new Methodlnvokerlnt(InqrementProgressBar), new object[] 1.);v Thread.Sleep(pictdrelntervalSeconds); • . pictureNumber++; -7. % •
256 Глава 7 } } ‘ • Invoke(new Methodlnvoker(ResetProgress)); * Invoke(new Methodlnvoke rBoolean(UIEnableStart), new object!] true); lnvoke(new MethodlnvokerBoolean(UIEnableStop), new object[] false); Делегат Met hod Invoker из пространства имен System. Windows. Forms уже обсуждал- ся, когда использовался в групповом приложении интерактивного форума. Делегат Methodinvoker позволяет вызвать методы, не принимающие параметров. Теперь так- же понадобятся методы, принимающие параметр типа int, string или bool, чтобы устанавливать конкретные значения и блокировать или разблокировать определен- ные элементы пользовательского интерфейса. Делегаты, достигающие эти цели, помещаются вверху файла PictureShowServer. cs: namespace Wrox.Networking Multicast { public delegate void Methodlnvokerlnt(int x); public delegate void MethodInvokerString(string s); public delegate void MethodInvokerBoolean(bool flag); Метод SendPicture() разбивает одно изображение на части, используя класс ути- лит PicturePackager. Каждое отдельное изображение преобразуется в массив байтов объектом Encoding типа UnicodeEncoding. Затем этот массив байтов отправляется по адресу группы рассылки методом Send() класса UdpClient: // PictureShowServer.cs ... ' - . - и Jf. private void SendPicture(Image image, string name, int index) { * J ' 1 ' / ' ' - ' W string message = “Sending picture ” + name; Invoke(new MethQdlhvokerString(SetStatusBar), 'к’ц new ObjectQ message); * PicturePackage!] packages - <-j - - v u PicturePackager.GetPictiirePackages(name, Index, image, 1024); f jf Отправляем все сегменты одного, изображения группе foreach (PicturePackage package in packages) { byte!} data = encoding.GetBytes(package.GetXml(j); int sendBytes = UdpClient.SendCdata, data.Length, groupEP); if (sendBytes < 0) MessageBox.Show(“Error sending”); Thread.Sleep(300); ' . ч.. 1 -• "У УУ У У&У. message « "Picture ” + name +< ’’ sent”; yyyh: Invoke!new MethodInvokerString(Set$tatusBar)r\ V•x . f new,object!] message);,
Сокетыгрупповой рассылки 257 Клиент приложения демонстрации изображений Пользовательский интерфейс клиента в приложении групповой рассылки изо- бражений имеет простой вид и состоит из формы с меню, панелью изображения и полосой состояния. На полосе состояния есть три панели, показывающие не толь- ко сообщения о состоянии, но и групповые адреса и номера портов. В элементе меню File содержатся подменю Start, Stop и Exit: Компоненты этой формы описаны в следующей таблице: Тип элемента Имя Комментарии Основное меню mainMenu Основное меню состоит из единственного элемента File с подменю Start, Stop и Exit. Панель изображения plotиreBox В этой панели выводится изображение, как только получены все составляющие его фрагменты. Полоса состояния statusBar При помощи свойства Panels полоса состояния разбита натри панели. В первой панели (statusBarPanelMain) показывается обычный текст состояния, вторая и третья панели (statusBarPanelAddress и statusBarPanelPort) предназначены для отображения группового адреса и номера порта. Класс формы называется PictureClientForm, он содержится в файле Pictu- reShowClient.cs. В него включены методы GetMulticastConfigurationO (для запроса с сервера информации о группе рассылки), OnStart() (для присоединения к группе рассылки) и Listener() (для запуска нового потока). Этот класс также содержит ме- тод DisplayPicture(), вызываемый для каждой демонстрируемой картинки, и метод OnStop(), который завершает слушающий поток. Начнем с пространств имен и закрытых полей, необходимых классу клиента PictureClientForm: •Л// PiCtUr6ShowClient.es using System;
258 Глава 7 uspig System.Drawing; using System.Collections; using System.Windows.Forms; using System Collections.Specialized; using System Net, using System Het.Sockets, using System.Threading; using System.Text; using System Configuration; namespace Wrox.Networking.Multicast 1 { oublic delegate void Methodlnvokerlnt(int i); public delegate void- MethodInvokerString(string s);. public: class PictureClientFofiJt’ System, Windows.Forms. Form f private IPAddress groupAddress;// Адрес группы расСылки private int groupPbrt; // Порт группы рассылка private int ttl, private UdpCIient udpClient; // Клиентский сокет для получения данных private string serverName; ' // Имя хоста сервера private int serverlnfoPort; ' // Пост для мнформад> о группе • , ' г,.'* 1 ;; _ • "* ’ ' ' * • -4' private’bool lone false:. r. JI Флаг завершения потока, получающего данные private UnicndeEncoding encoding •« new UnicodetncOdingO; J J Массив всех полученных изображений private SortedLis.t pictureArray. = new SortedlistO; Получение адреса группы рассылки Как в приложении группового интерактивного форума, для настройки клиента приложения демонстрации изображений используем конфигурационный файл. В данном случае он используется для конфигурирования имени и адреса сервера, чтобы клиенты могли соединяться с сокетом TCP, возвращающим адрес и номер порта для группы рассылки: <?knii versions’ll 0" епсоб1пд="1ЛТ-8'ь ?> «^configuration* <app$ettings> <add key="ServerName" vaiue=" localhost* /> tada key= ServerPort" values'7777' /> <aud keys”TTr values”32” /> </appSettirgs> - • «Anntiguration* 4 ... ...^ Значения этого конфигурационного файла считываются конструктором класса формы PictureClientForm, который также вызывает метод GetMulticastConfigu- ration(): // PictureShowClient cs public Pictured ientFormO { П /I Требуется для поддержки дизайнера Windows-форм И InitializeComponent();
Сокеты групповой рассылки 259 try { л ' >' // Считываем конфигурационный файл приложения NameValueCollection configuration = Configurationsettings.AppSettings; serverName й configurationCServerName”}; serverlnfoPort =» int.Parse(configuration[‘,ServerPort”]); ttl « int.Parse(configuration!"TTL"]); } • - . -Л. catch { . ' ; MessageBox.Show("Check the configuration file”); } V, • . ~ ♦’ ... '->!' ' J- GetMulticastConfigurationO: В методе GetMulticastConfiguration() соединяемся с сервером. Установив соеди- нение, можно вызывать метод Receive(), так как сервер после соединения сразу же отправляет ответ. Полученный массив байтов преобразуется в строку с помощью объекта класса UnicodeEncoding. Эта строка содержит адрес и номер порта для груп- повой рассылки, разделенные двоеточием (:), поэтому разбиваем строку на две час- ти и устанавливаем переменные-члены groupAddress и groupPort. Как упоминалось выше, в полосе состояния есть две дополнительные панели statusBarPanelAddress и statusBarPanelPort, в которых отображаются групповой ад- рес и номер группового порта: // PictureShowGlient.cs private void GetMulticastConfigurationO * Socket socket = new Socket(AddressFamily InterNetwork, SocketType.Stream, ProtocolType.Tcp); , try { 11 Получаем информацию о конфигурации группы рассылки от сервера IPHostEntгу server = Dns.GetHostByName(serverName); socket.Connect(new IPEndPoint(server. AddressList[O], serverlnfoPort)); byte[] buffer * new t>yte[6T2].; int receivedBytes = socket.Receive(buffer); . if (receivedBytes < 0) { MessageBox.Show(“Error receiving”); return; } socket.Shutdown(SocketShutdown.Both); string config = encoding.GetString(buffer); stringC] multicastAddress = config. Split(‘ ); groupAddress = new IPAddress(long.Parse(multicastAddress(OJ)); groupPort = int.Parse(multicastAddress[1]); statusBarPanelAddress. Text = groupAddress, ToStringO; statusBarPanelPort.Text ? groupPort.ToStringO; <- catch (SocketException ex) { if (ex.ErrorCode == 10061) { '• : MessageBox. Show(this,' "No server can be-foundon-A^^
260 Глава? ’ + serverNawe + *, at port ” + serverInfoPort, ,*• % % ^Э^Еггог Picture Show”/ MessageBoxButtons.OK, /Л ' •-•’ MessageBoxTcon.Error),* KS* - - ч-' else { - f' MessageBox.Show(this, ex.Message, •’Error Picture Show", MessageBoxButtons.OK, MessageBoxIcon.Error); < ) гИ-.'л ) !>j.' . . . > " • finally 1 -A- , . 5 A-- v' socket. Closed; } } Присоединение к группе, получающей рассылку Получив адрес и номер порта групповой рассылки, можно присоединиться к этой группе. Конструктор класса UdpClient создает сокет, слушающий порт груп- повой рассылки, и мы присоединяемся к группе рассылки, вызывая метод JoinMulticastGroupO. Задача получения сообщений и упаковки изображений принадлежит новому слу- шающему потоку, который запускает метод Listener(): л Z/Pictu reShowClient.es ' • *' j/Й private void OnStart(object sender, System. EventArgs.e) /-v udpCltent = new UdpCU«nt(g roupPort): 'J: W UdpClient.JoinMulttcastCroupCgroupAddress, ttl)? catgh (Exception ex) ' , Xf . 7-Л. ’4“: MessageBox. Show( ex. Message); ' ' ' • • , ‘ Thread tl = new Thread(new ThreadStart(Listener)); tl. Name = "Listener”;. tLlsBackground -₽ true; ti.Startd; ) Получение данных изображений, рассылаемых группе Вызвав метод ReceiveO объекта UdpClient, слушающий поток ждет, пока будут получены данные. Полученный массив байтов при помощи объекта кодирования преобразуется в строку, и по XML-строке, возвращенной объектом кодирования, инициализируется объект PicturePackage. Чтобы создать для клиента завершенное изображение, надо объединить все фрагменты одной картинки. Для этого используем переменную класса pictureArray типа Sorted! 1st. Ключом в этом отсортированном списке является идентификатор изображения, значением — массив объектов PicturePackage, составляющих завер- шенную картинку. Проверим, содержит ли уже pictureArray массив объектов PicturePackages для полученного изображения. Если содержит, фрагмент добавляется к массиву, иначе создаек^мовый массив объектов PicturePackages.
Сокеты групповой рассылки 261 После вывода на полосе состояния информации о фрагменте изображения и если все фрагменты картинки уже получены, вызываем метод Display Pict и ге(): // PictureShowClient. cs private void Listener() while (Idone) sa { ' ' • " // Получаем сегмент изображения для группы рассыпки IPEndPbiht ер - npll; ✓ < -• bytef] date * udpClient.Receive(ref ер); PicturePackage package = new Picturepackage! " coding. GetStrlng(data)); PicturePackage!] packages; if (pictureArray.ContainsKey(package.Id)) packages = (PicturePackage[DpictuceAr raylpackage.Id]; packages[package.SegmentNumber - 1] = package; } ’ ‘ sj. - else packages = new PicturePackage!package.NumberOfSegments]; packages!package SegmentNunber - I] ~ oackage; pictиreArray.Add(package.Id, packages); } •> --sc 7 ' string message = "Received picture ’’ + package.Id + “ Segment ’’ + package.SegmentNumber; Invoke (new MethodlnvokerStr.ing (SetStatusBa r), new object!] message); // Проверяем, все ли сегменты изображения получены int segmentcount = 0; ,»-.- •' ,.r. Л' for (int i ’ 0; i < package.NumberOfSegments; it+) if (packages[i] null) . / segmentCount++; // Все сегменты получены, прзтому рисуем картинку if (segmentcount == package.NumberOfSegments) { ' A. /*' < ttris.Invoke(new Methodlnvokerlht(DisplayPicture), - " \ new object!] package .Id); } ' > ‘ } В методе DisplayPicture( )нужно лишь воссоздать изображение, воспользовав- шись классом утилиты PicturePackager. Картинка отображается в панели изображе- ния формы. Поскольку фрагменты изображения больше не нужны, можно освободить немного памяти, удалив элемент, представляющий массив объектов PicturePackage, из коллекции отсортированного списка: private void DisplayPicture(int id) PicturePackage!] packages = (PicturePackage[])pictureArray[id];
262 Глава 7 . <4- Ima9e Picture = PipturePackager.GetPictUre(packages); » <"4=4 pictureArray. Remove(id); plotиreBox.Image = picture; Запуск демонстрации изображений Теперь мы готовы запустить серверное и клиентское приложения и выполнить демонстрацию изображений. Сначала нужно выбрать картинки с помощью кнопки Select Pictures... в интерфейсе сервера. Следующий рисунок показывает диалоговое окно Configure Pictures после выбора нескольких картинок: Щелчок по кнопке Init в основном диалоговом окне запускает поток InfoServer, слушающий запросы клиентов. Прежде чем запустить этот поток, можно изменить параметры групповой рассылки, но и установленных по умолчанию значений доста- точно, чтобы приложение могло работать. Как только поток InfoServer заработал, клиенты могут присоединяться к сеансу, выбирая в своих меню пункт File | Start. На снимке экрана показано серверное приложение в действии:
Сокеты групповой рассылки 263 Клиентское приложение сообщает о каждом поступившем сегменте изображе- ния в полосе состояния, слева от адреса и номера порта группы рассылки: Итоги В этой главе мы рассмотрели архитектуру и проблемы групповой рассылки и узнали, как можно реализовать эту возможность с помощьк) классов .NET. Групповая рассылка — это довольно молодая технология, которая имеет яркое будущее. Многие проблемы, связанные с безопасностью и надежностью, весьма ско- ро получат разрешение в стандартах. Для настоящей жизнеспособности средств групповой рассылки в Интернете требуется реализовать всего одно-два усовер- шенствования. * ; , Однако уже сейчас групповая рассылка имеет многочисленные полезные при- ложения, и ее применение должно значительно расшириться в грядущие годы. Использование групповой рассылки для отправки данных в сценарии “один со мно- гими” или для групповых приложений “многие со многими” существенно сокращает загрузку сети по сравнению с однонаправленной передачей. Обсудив архитектуру групповой рассылки, мы реализовали два приложения с применением этого средства. Первым было приложение интерактивного форума, соответствующее сценарию “многие с многими”, а вторым — сервер изображений, работающий в сценарии “один с многими”, отправляющий группе объемные дан- ные. На их примерах мы продемонстрировали, насколько легко встроенные мето- ды класса UdpClient реализуют в .NET групповую рассылку.
ГЛАВА 8 HTTP В предыдущей главе рассказывалось, как классы из пространства имен Sys* tem.Net помогают обеспечить полное решение при написании сетевых прило- жений в управляемом коде. В данной главе мы увидим, как платформа .NET осуществляет надежную реализацию протокола HTTP. Протокол HTTP — чрезвы- чайно важный прикладной протокол, поскольку в настоящее время он используется значительной частью Web-трафика. Классы .NET поддерживают большинство средств протокола HTTP 1.1. Его дополнительные средства включают конвейерную обработку, разделение на порции, аутентификацию, предварительную аутенти- фикацию, шифрование, поддержку прокси-сервера, серверную проверку сертифи- катов, управление соединениями и расширения HTTP. Платформа .NET также поддерживает создание приложений, использующих протоколы Интернета для от- правки и получения данных. В этой главе рассматриваются следующие вопросы: □ Обзор протокола HTTP — HTTP-заголовки, формат запросов и ответов. □ HTTP в .NET — использование классов HttpWebResponse/HttpWebRequest, WebClient, ServicePoint и ServicePointManager. □ Считывание и запись cookie в .NET. О Создание HTTP-сервера с поддержкой ASP.NET. । □ Использование транспортного канала Н'ГГР с .NET Remoting. Обзор протокола HTTP С наступлением эпохи World Wide Web с 1990 г. использование Интернета неве- роятно расширилось. Подобное расширение стало возможным после появления HyperText Transport Protocol (HTTP). Тим Бернерс-Ли впервые реализовал прото- кол HTTP в 1990-1991 гг. в CERN, Европейском центре по ядерным исследованиям в Женеве, Швейцария. HTTP — это упрощенный протокол прикладного уровня, который размещается поверх TCP и в основном известен как транспортный канал для World Wide Web
и локальных интрасетей. Однако это классический протокол, который использует- ся йомимо гипертекста для многих других задач, например, в серверах доменных имен и системах распределенного управления объектами посредством своих ме- тодов запросов, кодов ошибок и заголовков. Сообщение HTTP представляется в MIME-подобном формате; оно содержит метаданные о сообщении (например, тип его содержания и длину) и информацию о запросе и ответе, например, метод, ис- пользуемый для отправки запроса. Существуют два основных компонента, от которых зависит Web: сетевой прото- кол TCP/IP и HTTP. Почти все события в Web происходят через HTTP, и этот про- токол преимущественно используется для обмена документами (такими, как Web-страницы) в World Wide Web. HTTP — это протокол приложения клиент-сервер, через который взаимоде- йствуют две системы, обычно использующие соединение TCP/IP. HTTP-сервер — это программа, слушающая на порте машины входящие НТТР-запросы. HTTP-клиент через сокет открывает соединение с сервером, отправляет сооб- щение с запросом на конкретный документ и ждет ответа от сервера. Сервер от- правляет сообщение, содержащее код нормального или аварийного завершения, заголовки с информацией об ответе и (если запрос обработан успешно) требуемый 1 документ. Общий формат HTTP-сообщения одинаков для запросов и ответов: начальная-строка заголовок-сообщения (или заголовки) [тело-сообщения] В сообщение может входить любое число заголовков, и каждый из них распола- гается на отдельной строке (т. е. каждому заголовку Предшествуют символы возвра- та каретки и перевода строки). Тело сообщения присутствует необязательно, но если оно имеется, то отделяется от заголовков двумя последовательностями CRLF. В протоколе HTTP используются постоянные и непостоянные соединения. Не- постоянные соединения применяются по умолчанию в версии 1.0 HTTP, в то время как постоянные соединения—в версии HTTP 1.1. Соединение называют непостоян- ным (non-persistent connection) , если любое TCP-соединение закрывается сразу же, как только сервер отправляет клиенту требуемый объект. Это означает, что соеди- нение используется только для одного запроса и одного ответа и не сохраняется для других запросов и ответов. В случае постоянных соединений сервер, отправив ответ, оставляет соединение открытым, и, таким образом, следующие запросы и ответы между теми же клиентом и сервером могут отправляться через это же самое соединение. Такое соединение сервер закрывает лишь после того, как оно не используется в течение некоторого интервала времени. Рассмотрим вкратце пример запроса от HTTP-клиента (например, Web-браузе- ра) и ответ от Web-сервера (элементы этого запроса детально будут обсуждены позже). Следующее сообщение представляет собой запрос на страницу http://www.dotnetforce.com/default.aspx, отправленную из браузера IE 6: GET /default.aspx HTTP/1.1 Connection: Keep-Alive User-Agent: Mozilla/4.0 (compatible: MSIE 6.0; Windows NT 5.1; .NET CLR 1.0.3705) Host: www dotnetforce.com Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */* <CRLF><CRLF>
266 Глава 8 В первой строке задается используемый метод HTTP, относительный адрес по- лучаемого документа и текущая версия протокола HTTP. Далее следует набор заго- ловков, содержащих информацию о запросе. В данном случае запрос не включает тело сообщения, поэтому он заканчивается двумя последовательностями CRLF. Ответ на этот запрос от Web-сервера IIS 5 может выглядеть так: НТТР/1.1 200 ОК Server: Microsoft-IIS/5.0 Date: Thu, 08 Aug 2002 19:07:29 GMT Content-Type: text/html Accept-Ranges: bytes Last-Modified: Tue, 22 May 2001 11:19:22 GMT ETag: “7e9623db1e2c01:a3d” i Content-Length: 2499 <html> <! — здесь находится HTML-содержание —> </html> В ответе первая строка содержит версию HTTP, код состояния и сообщение. За ней следуют заголовки сообщения, пустая строка и тело сообщения. Обычно тело сообщения представляет собой содержание запрошенного документа (или содержа- ние, сгенерированное страницей ASP.NET или сценарием со стороны сервера). НПР-заголовки Как видно, HTTP-сообщение состоит из начальной строки, за которой следуют набор заголовков, пустая строка и некоторые данные. Начальная строка задает де- йствие, требуемое от сервера, тип возвращаемых данных или код состояния. HTTP-заголовки можно подразделить натри крупные категории: заголовки, по- сылаемые в запросе, заголовки, посылаемые в ответе, и те, которые можно вклю- чать как в запросы, так и в ответы. Заголовки запросов указывают возможности клиента, например, типы документов, которые может обработать клиент, в то вре- мя как заголовки ответов предоставляют информацию о возвращенном документе. Заголовки запросов К числу наиболее важных Н ГI P-заголовков, которые можно включать в запро- сы, но нельзя включать в ответы, относятся: □ Заголовок Accept — это список MIME-типов, принимаемых клиентом, в фор- мате тип/подтип. Элементы списка должны разделяться запятыми: Accept: text/html, image/gif, */* Элемент */* указывает, что все типы будут приняты и обработаны клиентом. Если тип запрошенного файла не может быть обработан клиентом, возвраща- ется ошибка HTTP 406 "Not acceptable" (недопустимо). □ Заголовок From указывает адрес электронной почты в Интернете учетной за- писи пользователя, под которой работает клиент, направивший запрос: From: response@dotnetforce.com □ Заголовок Referer позволяет клиенту указать адрес (URI) ресурса, из которо- го получен запрашиваемый URL Этот заголовок дает возможность серверу сгенерировать список обратных ссылок на ресурсы для будущего анализа, ре-
, И71? . Д _ ?67 гистрации, оптимизированного кэширования и т. д. Он также позволяет про- слеживать с целью последующего исправления устаревшие или введенные с ошибками ссылки: Referer: http://ww.dotnetforce.com/Default.aspx О Заголовок User-Agent представляет собой строку, идентифицирующую при- ложение-клиент (обычно браузер) и платформу, на которой оно выполняет- ся. Общий формат имеет вид: лрограмма/версия библиотека/версия, но это не неизменный формат. Для IE б эта строка выглядит так: User-Agent: Mozilla/4.0 (compatible, MSIE 6 0; Windows NT 51; .NET CLR 1.0.3705) Эта информация может использоваться в статистических целях, для отслежи- вания нарушений протокола и для автоматического распознавания клиента. Она позволяет приспособить ответ так, чтобы не нарушить ограниченные возможности конкретного клиента, например неспособность поддерживать HTML-таблицы. Заголовки ответов В ответы могут включаться следующие заголовки: □ Заголовок Content-Туре используется для указания типа данных, отправляе- мых получателю, или, в случае метода HEAD, тип данных, который был бы от- правлен в ответ на запрос GET: Content-Type: text/html □ Заголовок Expires представляет собой момент времени, после которого ин- формация в документе становится недостоверной. Клиенты, использующие кэширование, в частности прокси-серверы, не должны хранить в кэше эту ко- пию ресурса после заданного времени, если только состояние копии не было обновлено более поздним обращением к исходному серверу: Expires: Fri, 09 Aug 2002 16:00:00 GMT □ Заголовок Location определяет точное расположение другого ресурса, к кото- рому может быть перенаправлен клиент. Если это значение представляет собой полный URL, сервер возвращает клиенту “redirect” для непосредствен- ' ного извлечения указанного объекта: Location: http://www.dotnetforce.com/WS/Default.aspx Если ссылка на другой файл относится к серверу, должен указываться частич- ный URL: Location: /Tutorial/HTTP/index.html □ Заголовок Server содержит информацию о программном обеспечении, ис- пользуемом исходным сервером для обработки запроса: Server: Microsoft-IIS/5.0
268 Глава 8 Общие заголовки Несколько заголовков могут включаться как в запрос, так и в ответ, например: □ Заголовок Date используется для установки даты и времени создания сообще- ния: Date: Тие, 06 Aug 2002 18:12:31 GMT □ В HTTP/1.0 мы могли использовать в запросе заголовок Connection, указывая, что хотим сохранить соединение после отправки ответа. Теперь такое пове- дение принято по умолчанию, и в HTTP/1.1 можно использовать заголовок Connection, чтобы указать, что постоянное соединение ненужна Connection: close HTTP-запросы Каждый клиент посылает запрос, и сервер на него отвечает. Все запросы и отве- ты состоят из трех частей, а именно: строки запроса или ответа, секции заголовков и тела сущности (любого содержания, отправляемого вместе с сообщением, напри- мер, страницы HTML для отображения в браузере или данных формы, пересылае- мых на сервер). Клиент связывается с сервером в назначенном номере порта (по умолчанию равном 80) и запрашивает у сервера документ, задавая НТТР-команду, называемую методом, за которой следует адрес документа и номер версии HTTP. Клиент также отправляет серверу необязательную информацию в заголовках, что- бы сообщить серверу о своей конфигурации и приемлемых для него форматах доку- ментов. Информация заголовка дается в одной строке вместе с именем и значением заголовка. После заголовков клиент посылает пустую строку. Затем клиент отправ- ляет дополнительные данные. Это могут быть данные формы, отправляемые на сер- вер методом POST, или файл, копируемый на сервер методом PUT. Запросы клиентов подразделяются на три секции. Первая строка сообщения всегда должна содержать HTTP-команду, называемую методом, за которой следует URI, идентифицирующий файл или ресурс, запрашиваемый клиентом, и номер вер- сии HTTP: GET /default.aspx HTTP/1.1 Теперь исследуем каждую из этих секций. Метод—это HTTP-команда, начинаю- щая первую строку запроса клиента. Метод информирует сервер о цели запроса кли- ента. Для HTTP определены семь методов: GET, HEAD, POST, OPTIONS, PUT, DELETE и TRACE, но HTTP-серверы могут также реализовать методы расширения, не опреде- ленные протоколом HTTP. Заметим, что названия методов зависят от регистра кла- виатуры, поэтому, например, слово get не будет распознано как допустимый метод. Метод GET используется для запроса информации, расположение которой на сервере определяется заданным URI. Этот метод широко применяется браузерами, чтобы извлекать документы для просмотра. Результат запроса GET генерируется раз- ными способами. Это может быть файл, доступный с сервера, вывод программы, вывод, полученный на устройстве, и т. д. Когда клиент в своем запросе использует метод GET, сервер отправляет ответ, со- держащий строку состояния, заголовки и метаданные. Если сервер не может обработать запрос из-за ошибки или отсутствия авторизации, он отправляет объяс- нение в текстовом виде, помещая его в ответе в секцию данных.
HTTP 269 Секция тела о сути запроса GET всегда остается пустой. Запрошенный клиентом ресурс (файл или программа) идентифицируется по его полному пути на сервере. Любая дополнительная информация, например, значения из формы, которую кли- енту нужно отправить серверу, присоединяется вслед за URI как строка запроса: GET / default.aspx?name=Vinod НТТР/1.1 Метод HEAD функционально аналогичен методу GET, не считая того, что сервер ничего не помещает в секцию данных ответа. Методом HEAD запрашивается только заголовочная информация по файлу или ресурсу. Для запроса HEAD HTTP-сервер должен отправить в заголовках ту же информацию, которую он бы отправил в ответ на запрос GET. Данный метод используется, если клиенту нужна информация о доку- менте, но не нужно получать сам документ. Метод POST позволяет отправить данные серверу в клиентском запросе. Эти дан- ные посылаются программе обработки данных, к которой у сервера есть доступ. Метод POST может использоваться для многих приложений, например, для обеспе- чения входных данных сетевых служб, программ интерфейса командной строки и т.д. Данные отправляются на сервер в секции тела сути клиентского запроса. Обра- ботав запрос POST и заголовки, сервер передает это тело программе, указанной в URL В методе OPTIONS запрашивается информация о поддержке HTTP на Web-серве- ре. Метод OPTIONS может применяться с URL, чтобы извлечь информацию о кон- кретном документе или, с групповым символом *, чтобы получить информацию о возможностях сервера в целом. Информация возвращается в заголовках ответа. В следующем ответе содержится информация, возвращаемая на запрос опций для страницы postinfo.html на сервере IIS 5 server (OPTIONS/postinfo html НТТР/1.1): НТТР/1.1 200 ОК Server: Microsoft-IIS/5.0 Date: Fri, 09 Aug 2002 18:52:18 GMT MS-Author-Via: DAV Content-Length: 0 Accept-Ranges: bytes DASL: <DAV:sql> DAV: 1, 2 Public: OPTIONS, TRACE, GET, HEAD, DELETE, PUT, POST, COPY, MOVE, MKCOL, PROPFIND, PROPPATCH, LOCK, UNLOCK, SEARCH Allow: OPTIONS, TRACE, GET, HEAD, COPY, PROPFIND, SEARCH, LOCK, UNLOCK Cache-Control: private <CRLF><CRLF> Заголовок Allow указывает, какие методы HTTP разрешены для данного кон- кретного документа, а заголовок Public сообщает обо всех методах, поддерживае- мых сервером. Например, приведенный ответ показывает, что метод DELETE поддерживается сервером, но не разрешен для postinfo html. В ответе также указывается поддержка сервером DAV (Distributed Authoring and Versioning) и DASL (DAV Searching and Locating). DAV- это стандарт, расширяющий HTTP/1.1, предоставляя новый набор методов, позволяющий управлять ресурсами на Web-cepeepe, например, устанавливать и считывать свойства ресурсов, перемещать и копировать файлы. DASL определяет расширения, позволяющие выполнять поиск на DAVcepeepax. DAV определен в RFC 2518 (http://www.letf.org/rfc/rfc2518.txt). Метод DELETE запрашивает удаление с сервера названного файла, а метод PUT — копирование на сервер документа, включенного в тело запроса. Документ, отправ-
270 Глава 8 ленный методом PUT, должен быть доступным под указанным URL Наконец метод TRACE используется для отладки цепочки запрос-ответ; сервер должен вернуть все со- общение запроса в теле ответа. После метода запроса следует URI, задающий файл, который хотим получить, или файл, в который хотим поместить отправленные данные, и т. д. Это может быть абсолютный URI, например http ://www.wrox.com/defautt.aspx, или путь относи- тельно корневого каталога документов сервера, например /default. aspx (перед от- носительным путем всегда ставится прямой слэш). Сервер должен устанавливать соответствие между этим URI и путем в файловой системе, например C:\inetpub\ wwwroot\ default.aspx. Последний компонент строки запроса—это номер используемой версии HTTP. Обычно это будет или НТТР/1.1 или HTTP/1.0 (хотя существует и более старая версия, НТТР/0.9). НПР-ответы Ответ сервера на запрос клиента также подразделяется на три части. Первая строка—это строка ответа сервера, Содержащая номер версии HTTP, номер, указы- вающий состояние запроса,' и краткую фразу, описывающую это состояние. Далее следует информация заголовков, за ней — пустая строка и тело сущности (которое может быть пустым, например, в ответах на запросы HEAD и OPTIONS). В качестве версии HTTP указывается та версия, которую сервер использует в от- вете. Код состояния представляет собой трехбайтовое число, указывающее резуль- тат обработки сервером запроса клиента. Описание, следующее за кодом состояния, просто дает удобное для восприятия пользователем значение кода со- стояния. Хотя существует несколько определенных кодов состояния, сервер вправе устанавливать дополнительные коды. Некоторые наиболее распространенные определенные коды приводятся в следующей таблице: Код Описание 200 0К — запрос был получен и обработан 301 Ресурс перемещен постоянно 302 Ресурс перемещен временно 400 Неверный запрос — сообщение с запросом имеет некорректный формат 401 Несанкционированный доступ — у пользователя нет прав для доступа к запрошенному документу. 402 Ресурс доступен за плату 408 Тайм-аут запроса 500 Внутренняя ошибка сервера—ошибка помещала HTTP-серверу обработать запрос После строки состояния сервер отправляет клиенту в заголовках информацию о себе и запрошенном документе. Заголовки завершаются пустой строкой (т.е. дву- мя идущими подряд последовательное гями CRLF). Если клиент запрашивал данные и запрос обработан успешно, эти данные будут отправлены в теле сущности после заголовков ответа. Они могут представлять со-
НИР 271 бой копию запрошенного файла или содержание, сгенерированное динамически, например страницу ASP.NET или сценарий на стороне сервера. Если запрос клиен- та не выполнен, могут быть предоставлены дополнительные данные, объясняю- щие, почему сервер не смог выполнить этот запрос. В HTTP 1.0 сервер, завершив отправку запрошенных данных, отсоединяется от клиента и транзакция на этом заканчивается, если только не был отправлен заголо- вок Connection. Keep-Alive. Однако в HTTP 1.1 сервер должен поддерживать соеди- нение, позволяя клиенту делать дополнительные запросы, даже если заголовок Connection не был отправлен. Если не нужно такое поведение, следует отправить за- головок Connection: close, который указывает, что после отправки ответа соедине- ние должно быть закрыто. НИР в .NET Хотя можно реализовать протокол HTTP вручную, воспользовавшись обычны- ми классами сокетов или TCP, платформа .NET предоставляет несколько классов (главным образом в пространстве имен System. Net), предназначенных облегчить взаимодействия с HTTP-сервером. В этих классах реализована общая модель за- прос-ответ, а также имеются дополнительные свойства, обеспечивающие более вы- сокий уровень управления специфическими средствами НТГР, например доступ к протоколу НИ Р в объектной модели для управления на уровне свойств заголовка- ми, аутентификацией, предварительной аутентификацией, шифрованием, под- держкой прокси-сервера, конвейерной обработкой и соединениями: Класс Пространство имен Наследован от Описание HttpWebRequest System.Net WebRequest Представляет HTTP-запрос HttpWebResponse System.Net WebResponse Представляет HTTP-ответ WebClient System.Net Component Предоставляет простые в употреблении методы для отправки файлов или данных заданному URI и получения данных от URI Uri System MarshalByRef Object Представляет URI, позволяя легко обращаться к частям URI, например, к имени хоста и абсолютному пути UriBuilder System Object Класс утилит для создания и модификации объектов Uri ServicePoint System.Net Object Обрабатывает соединения с заданным URI Se rvicePointManage r / System Net Object Класс менеджера для управления объектами ServicePoint
272 Глава 8 HttpWebRequest и HttpWebResponse Среда .NET Framework предоставляет два основных класса, облегчающих доступ к HTTP: HttpWebRequest и HttpWebResponse. В этих классах в простой форме реализова- на значительная часть функциональных возможностей, предоставляемых прото- колом HTTP. Они порождены от абстрактных классов WebRequest и WebResponse, которые рассматривались в главе 3. Чтобы познакомиться с работой этих классов, давайте рассмотрим пример, как их можно использовать для получения страницы из Интернета: using System; • Жд ' using System.Net; t Л * using System.10; using System. Text; ,„f, ‘ Ф&Х glass SimpleWebRequest- .л</М" /’ « . public static void Main() •/ .... -i . ‘ 7 * string query = ‘'http://www.wrox.com*'; HttpWebRequest req - (HttpWebRequest )HttpWebRequeSt.Create(auery); HttpWebResponse resp = (HttpWebResponse)req.GetResponseO r’ 7 StreamReader sr = new StreamReader(resp.GetResponseStream(), : ... • Encoding.ASCII!; Console. WriteLineCsr. ReadToEndC)) •; resp. Closet); sr.CloseO; Для создания объекта HttpWebRequest нужно вызвать статический метод Create() класса WebRequest (также наследуемый классом HttpWebRequest). Этот метод исследует формат переданного ему URI и по обстановке возвращает объект WebRequest, пред- ставляющий HTTP-запрос или запрос к файловой системе. Поскольку один и тот же метод используется для создания как HTTP-запроса, так и запроса к файловой систе- ме, возвращаемый объект имеет тип WebRequest и его нужно привести к типу HttpWebRequest или FileWebRequest. Метод Create() выполняет разбор URL и переда- ет его в объект запроса. Затем блок запроса строит исходящий HTTP-запрос и также формирует конфигурацию НТТР-заголовков. Получив объект запроса, можно вызвать для него метод GetResponse(). Этот ме- тод отправляет запрос серверу и возвращает объект WebResponse (надо снова при- вести его к типу HttpWebResponse). Этот объект представляет сообщение HTTP-ответа и содержит информацию НТТР-заголовков, в том числе ContentType, Content Lengt h, StatusCode и Cookies, и первую часть данных, которые помещаются во внутренний буфер и должны быть считаны из потока. Вместе с данными устанавли- ваются свойства объекта HttpWebResponse. Далее с помощью метода GetResponseSt ream() получаем поток. Он указывает на текущий ответ от Web-сервера (тело сути ответного сообщения) в двоичном виде. Поток предоставляет значительную гибкость в реализации способов получения данных от Web-сервера. Чтобы извлечь текущие данные и получить с WetxepBepa оставшуюся часть результирующего документа, нужно считать этот поток. В данном примере используем объект St reamReade г, возвращающий строку с данными. Для ко- дировки установлен тип ASCII (хотя более надежным было бы решение проверить заголовок Content-Encoding и воспользоваться заданной в нем кодировкой). Коди- ровка — это важный момент, поскольку передача данных потоком байтов без пере- кодирования приведет к неверным преобразованиям всех расширенных символов.
НИР 273 В приведенном примере используется метод ReadToEndO объекта StreamReader, считывающий в строку все данные с Web-сервера. Однако данные можно также счи- тывать по частям, если применить метод Read() того же объекта. Более подробно о считывании данных из потоков рассказывалось в главе 2. Установка и считывание НТТР-заголовков В обоих классах, HttpWebRequest и HttpWebResponse, есть свойство Headers, воз- вращающее объект WebHeaderCollection, содержащий информацию о заголовках HTTP-сообщения. Вызывая метод Add() или метод Set() этого объекта, можно до- бавлять заголовки в НТТР-запрос: HttpWebRequest req = (HttpWebRequest)WebRequest.Create( - ! I# • л “http://localhost«81") r . req.Headers.Add(“Accept-Language: en-us”); \ | Это в точности равнозначно следующему вызову «И» йп//. req.Headers^Set( “Accept-Language” (’en-us” я Как показывает приведенный пример, метод Add() принимает один строковый параметр, представляющий все поле заголовка, тогда как метод Set () принимает два строковых значения: имя заголовка и значение, которое хотим в нем устано- вить. Оба метода можно равным образом использовать для стандартных и спе- циализированных НТТР-заголовков. Однако в классе HttpWebRequest также есть несколько открытых свойств, позволяющих установить заголовок запроса без обра- щения к свойству Heade rs: Свойство Тип данных НИР-заголовок Accept string Accept Connection string Connection ContentLength long Content-Length ContentType string Content-Type Expect string Expect IfModifiedSince DateTime If-Modified-$ince Referer string Referer TransferEncoding string Transfer-Encoding UserAgent string User-Agent Например, чтобы сформировать заголовок User-Agent в виде "User-Agent: SimpleHttpClient", можно написать: req.UserAgent = "SimpleHttpClient”; Обратите внимание, что при наличии свойства установки заголовка следует обращаться именно к нему. Например, если попытаться установить заголовок User-Agent следующим образом: req.Headers.Add(“Useг-Agent: SimpleHttpClient”); то при выполнении будет порождено исключение.
274 Глава 8 Объект HttpWebResponse также содержит несколько открытых свойств (все они доступны только на чтение), позволяющие обращаться к значениям выбранных HTTP -заголовков: Свойство Тип НИР-заголовок ContentEncoding string Content-Encoding ContentLength long Content-Length ContertType string Content-Type LastModified DateTime Last-Modified Server string Server К каждому отдельному полю заголовка также можно обратиться как к паре имя-значение в коллекции WebHeaderCollection. Имена заголовков являются ключа- ми в коллекции, а значения заголовков — соответствующими значениями. Следова- тельно, можно обратиться к значению заголовка, используя в индексаторе коллекции имя заголовка. В следующем коде все заголовки ответу выводятся в окно консоли: ’ foreach (string header in resp.Headers) ft Console.WriteLine(‘‘(O}: {!}”, header,, resp.Headers[header])z Приложение перевода валют Мы узнали, как получить содержимое из Web, пользуясь объектами HttpWeb- Request и HttpWebResponse, а теперь рассмотрим пример приложения, в котором эти объекты используются для преобразования валюты одного вида в другую с помо- щью текущих валютных курсов с Web-сайта http://finance.yahoo.com: using System; using System.IO;; using System.Net; using System.Text; class Currencyconverter , static void MainQ > HttpWebRequest req; '?/• HttpWebResponse resp; StreamReader sr; char[] separator = { \’ }; string result; string fullPath; ' string currencyFrom = “USD”; // доллар США string currencyTo = “INR”; , If индийская рупия doubleamount.- 100d; > ч > Console.WriteLine("Currency Converter");, ? . r Console.WriteLine(“Currency From ; {0}’’, сц-ггепсуЕгот); Console.Writeline(“Currency To : {0}% currencyTo); Console.WriteLine("Amount : {О}”, amount); // Построим URL, возвращающий .котировку < :etz
HTTP 275 fullPath = “http://finance,yahoo.com/d/quotes.csv?s=” + currencyFrom + currencyTo + ,,=X&f=sl1d1t1c1ohgv&e=. csv”; ''^try ЛМ. ' S?". ч -г ' л req * (HttpWebRequest)WebRequest.Create(fullPath); resp = (HttpWebResponse)req.GetResponse(j; sr = new StreamReader(resp.GetResponseStream()„ Encoding.ASCII); result = sr.ReadLine(); resp.CloseO;- !/ sr, CloseO; stringf] temp result.Split(separator); if(temp.Lengtn >1) f ;» ,s'-. it A t . J: i.4 Фз'л // Показываем только значимые части double rate = Convert.ToDouble(temp[1]); double convert = amount, * rate, Console.WriteLin.e(“<Q) 0}(s) = {2} {3}(s)”( amount, currencyFrom, convert., currencyTo); } else { ! ~ •- -• ” Console.WriteLine(“Error in getting currency rates ’’ + ‘ “from website."); } } catch(Exception e) { > Console.WriteLineC*Exception occurred’’); } Получим текущее соотношение валют, создавая URL, указывающий на Web-сайт finance.yahoo.com и включающий строку запроса, задающую валюты, которые хо- тим конвертировать, в виде трехсимвольных строк (в данном примере USD для долларов США и INR для индийских рупий) вместе с некоторой загадочной инфор- мацией, требуемой сервером Yahoo. Для отправки этого запроса на сервер исполь- зуем объект HttpWebRequest, а для получения доступа к возвращенному документу — объект HttpWebResponse. Этот документ представляет собой файл с разделенными за- пятыми значениями (CSV), и в нем второе значение — это текущий курс обмена ва- лют. Нас интересует только данное значение, поэтому преобразуем его в тип double и сохраняем в переменной rate. Затем используем ее для вычисления значения пре- образованной валюты:
276 Глава 8 Заметьте, что этот пример может работать неверно в некоторых странах, где точка (.) используется не как десятичная точка, а как разделитель тысяч Для устранения некорректности можно было бы перед вызовом метода Convert. ToDoubleO для значения, считанного из CSV-файла, заменить десятичную точку на запятую: double rate = Convert.ToDouble(temp[1].Replace(“.”, Более надежным решением была бы проверка свойства CurrencyDecimalSeparator объекта NumberFormatlnfo для данной валюты. Отсылка данных на сервер Приложение перевода валют получает данные запросом HTTP GET. Если нужно отослать данные на сервер, используется метод HTTP POST. Под отсылкой (POST) дан- ных понимается процесс получения данных и отправки их Web-серверу вместе с за- просом. Операция POST не только отправляет данные серверу, но также извлекает ответ сервера, в котором указывается успешное или неудачное выполнение запроса и, возможно, содержатся другие данные, например Web-страница. using System. using System.IO; using System.Net; using System.Text; using System.Web; class PostData < static void Main() { string SiteURL="http://localhost/postsample.asox ; Streamwriter sw = null; // Готовим данные k отсылке Л string postData = “Posted?»* + HttpUtility. UrnEncodeCTrue") +, “&X*” * HttpUfcijny.OrlFneodeC’Value*^' , f?’ ' « Iw’’ .. . .HttpWebRequest req ~ (HttpWebRequest)WeoRequest.Create(SiteURL); req.Metho. * “POST”; , ... > r req. Content Length = oostData. Length, . / req. ContentType = “appIication/x-www-fprm-urlencodedMi sw * new StreamWriter(req GetRequestStream()); // Перекодировка данных byte[] sendBuffer « Encoding.ASCII.GetBytes(postData); // ChXWKa данных r. sw.Write(postData); > “ *'« sw^loset); HttpWebResponse resp я (HttpWebResponse) req. GetResponseO; . StreamReader srData * new StreamReadprCresp GethesponseStreamC), // ‘Счйтывейие ‘выходного :п0тока'Е string outHtml '= srbata. BeadtoEndf)’ ..j Console.WriteLine(6utHtmr);: Z/ Закрьпие и очистка StreamReader
НИР 277 resp. Close О; srData.Close(); } } Отсылаемые данные нужно должным образом перекодировать перед отправкой серверу. Данные, передаваемые Web-странице, могут иметь один из двух MIME-ти- пов: application/x-www-form-urlencoded (как в этом примере) или multipart/form-data. В первом случае данные кодируются так же, как строка запроса, — данные должны кодироваться в буфере в парах ключ-значение, и для этих значений надо использо- вать URL-кодировку. Для кодирования данных можно вызвать статический метод UrlEncodeO класса System. Web. HttpUtility. Тип multipart/form-data позволяет копи- ровать файлы с клиента через HTML-форму. Как можно предположить по названию этого типа, данные разделяются на секции, содержащие двоичные или символьные данные или данные формы в обычной URL-кодировке. Заметим, что URL-кодировка должна использоваться только при отсылке данных Web-странице. Ни для какого другого содержания этого делать не нужно - можно отсылать данные сразу. Отсылаемые данные должны быть перекодированы и сохранены в массиве бай- тов с использованием метода GetBytesO статического объекта Encoding. ASCII, воз- вращающего массив байтов. После установки свойства ContentLength, которое помогает удаленному серверу обработать размер потока данных, отсылаемые дан- ные передаются на сервер через объект Streamwriter. Этот объект записывает дан- ные в выходной поток, полученный вызовом метода GetRequestStream() объекта HttpWebRequest. Отправив данные, используем объект StreamReader для считывания ответа от сервера. До настоящего момента рассматривалось лишь элементарное применение объ- ектов HttpWebRequest и HttpWebResponse. Однако чтобы построить типичное приложе- ние на НТ ГР, нужно использовать некоторые дополнительные средства, такие, как KeepAlive, Connectionlimit, группы соединений и т. д. Теперь рассмотрим, как используются эти свойства, и познакомимся с некоторыми дополнительными средствами HTTP. Передача данных порциями в HTTP Разделение данных на порции (chunking) — одно из средств, добавленных в HTTP в версии 1.1. Имеется в виду процесс отправки тела сообщения не в одной операции, а несколькими фрагментами. Если установлено постоянное соединение и заголовок не включает поле Content-Length, каждому фрагменту должен предшест- вовать его размер. В противном случае получатель не сможет узнать, когда тело сообщения отправлено полностью и сообщение стало свободно для следующей транзакции. Кодирование порционной передачи может использоваться как запросами, так и ответами, хотя чаще оно применяется в ответах. Главным образом оно использу- ется, когда приложению нужно отправить или получить данные, чей точный раз- мер неизвестен в момент начала передачи с сервера или на сервер. Наиболее часто встречается ситуация, в которой данные создаются динамически в соответствии с логикой работы другого приложения или сервера. Для отправки данных порция- ми нужно установить значение true в свойстве SendChunked объекта HttpWebRequest: using System; using System; 10; . ' using System.Netj ** »' ’
278 Глава 8 using System.Text; class ChunkingExample { static void MainQ string Query « "http://local.host/postsample, aspx”? Streamwriter sw = null; string postData = “Posteditrue&X=Value"; HttpWebRequest req - (HttpWebRequest)HttpWebRequest.Create(query); 77 Установка метода запроса req.Method * “POST”; fl Установка заголовка Content-Type req.ContentType = "application/x-www-form-urlencoded'1; // Установка заголовка Content-Length req.ContentLength = postData.Length; // Установка свойства SendChunked req.SendChunked = true; // Отсылка данных sw = new StreamWriter(req.GetRequestStream()); sw.Write(postData); sw.CloseC); / HttpWebResponse resp » (HttpWebResponse)req.GetResponseC);, StreamReader sr = new StreamReader<resp.GetResponseStreamO); < * s' > И Считывание выходного потока string outHtml = sr ReadToEndO; Console.WriteLine(outHtml); ‘ resp. Closed; .,y ... sr. Closed; Конвейерная обработка в HTTP Одним из наиболее важных средств HTTP 1.1 является конвейерная обработ- ка. Это средство позволяет классам .NET одновременно отправить черёз постоян- ное соединение поддерживающему (back-end) серверу несколько HTTP запросов, не дожидаясь ответа от сервера перед отправкой следующего запроса. Таким обра- зом, если подключено средство конвейерной обработки, то приложение, запраши- вающее на сервере несколько ресурсов, не блокируется, ожидая доступа к одному конкретному ресурсу, передача которого может идти дольше остальных. Следова- тельно, производительность приложения может возрасти. На следующем рисунке демонстрируется использование конвейерной обработки по сравнению с традици- онным поведением HTTP в стиле запрос-ответ. По умолчанию конвейерная об- работка в .NET разрешена, но ее можно отключить, если установить в свойстве Pipelined объекта HttpWebRequest значение false. Можем это сделать, если важно до отправления следующего запроса получать ответ на предыдущий запрос.
НИР 279 Поддержка активного соединения HTTP Постоянные соединения, или поддерживаемые активными (keep-alive) соедине- ния были введены в НТТР/1.0, чтобы позволить клиенту, использующему HTTP, экономить сетевые ресурсы и действовать более эффективно, поддерживая актив- ным и используя существующее TCP-соединение, вместо того чтобы закрывать его и создавать новое соединение для очередного запроса. Заголовок Connection управляет сохранением соединения. По умолчанию все со- единения HTTP/1.1 считаются постоянными, пока в запросе или ответе не появит- ся заголовок Connection: close. Когда сервер или клиент получает сообщение с этим заголовком, он закрывает соединение по завершении транзакции. По умолчанию в свойстве KeepAlive объекта HttpWebRequest установлено зна- чение true. Это приводит к созданию постоянного соединения с сервером, при условии, что сервер поддерживает это поведение. Важно иметь в виду, что при обра- щении к поддерживающему серверу из приложения ASP.NET среднего уровня, сое- динение остается открытым, пока сервер не разорвет его по тайм-ауту. Если в свойстве KeepAlive установлено значение true, соединение не будет за- крыто, пока не истечет интервал тайм-аута до поступления нового запроса или сер- вер явно завершит соединение. В большинстве случаев как клиенту, так и серверу разрешается закрыть соединение через отправку в запросе или ответе заголовка Connection: close. Управление соединением НИР Управление соединением — важное средство в достижении максимальной мас- штабируемости и производительности сетевого приложения. Эти цели достига- ются ограничением числа установленных сокетов, ориентированных на вывод, и использованием для оптимизации взаимодействия клиент-сервер таких дополни- тельных средств HTTP, как постоянные соединения. Платформа .NETуправляет со- единениями через классы ServicePoint и ServicePointManager. Класс ServicePoint из пространства имен System Net управляет соединениями с ресурсом Интернета на основе информации о хосте, передаваемой в URI ресурса. Начальное соединение с ресурсом определяет информацию, которая должна под- держиваться классом ServicePoint, а он, в свою очередь, совместно используется все- ми последующими запросами к этому ресурсу. Объект ServicePoint представляет соединение с URI, имеющим конкретные идентификатор протокола (например, http://) и имя хоста (например, www.dotnetforce.com). Если два и более запросов обращаются к ресурсам с одним и тем же протоколом и одним именем хоста, то для всех этих запросов будет исполь- зоваться одно и то же соединение. Например, если обратиться с запросом к страни-
280 Глава 8 це http://www.dotnetforce.com/default.aspx, а затем второй запрос отправить к http://www.dotnetforce.com/SiteContent.aspx7Types1, то оба раза будет использо- ваться одно соединение, поскольку оба запроса содержат одинаковую информацию о хосте и один идентификатор протокола. Создадим экземпляр класса ServicePoint, используя статический метод FindSer- vicePoint() класса ServicePointManager. Он принимает объект Uri, представляющий ресурс Интернета, для соединения с которым будет использоваться эта точка обслу- живания, или же объект Uri либо URL в строковом формате вместе с объектом IWebProxy. Бегло обсудим вопросы, связанные с Web-прокси. Например: // Получаем o6beta Uri, указывающий на http://wwv: dotnetforce.com Uri siteURL = new Uri(“http‘//www. dutnetforce com”); // Вызываем FindServicePointO, чтобы получить ServicePoint для этого URI ServicePoint spSite = ServicePointManager. FindServicePoinUsiteURi.); Тайм-аут соединения Когда создается экземпляр ServicePoint, он поддерживает соединение с указан- ным ресурсом Интернета, пока не истечет тайм-аут. Можно менять значение этого тайм-аута для отдельного объекта ServicePoint, устанавливая свойство ServicePo- intManager.MaxServicePointldleTime. Кроме того, через свойство IdleSince выясним время, прошедшее с тех пор, как было установлено последнее соединение с точкой обслуживания: // Получаем время существования соединения DateTime idleTimo = spSite.IdleSince; // Установка MaxldleTime в миллисекундах spSite. MaxIdleT ime = 5000; Можно также установить в MaxldleTime значение Timeout. Infinite (из простра- нства имен System.Threading), чтобы указать, что объект ServicePoint никогда be должен закрываться по тайм-ауту (хотя соединение, конечно, может завершаться сервером): sj-Jite. MaxldleTime = Timeout. Infinite; Предельное число соединений По умолчанию максимальное число соединений с данным сервером, разрешен- ных для приложения, использующего класс HttpWebRequest, равно двум. Это число можно увеличивать или уменьшать в зависимости от реальных условий, в которых выполняется приложение. В следующем коде максимальное число соединений кли- ент устанавливается равным четырем: Uri SiteURL = new Uri('‘hnp://www.dotnetforce.com”), •». ServicePoint spSite = ServicePointManager.FindServicePoint(SiteiJRL)i spSite.ConnectionLimit = 4; Максимальное число используемых соединений зависит от условий, в которых выполняется приложение. Поэтому лучше начать с оценки эффективности работы приложения со значением, принятым по умолчанию, а затем изменять это значение
НИР 281 и наблюдать за влиянием на производительность. Например, полагая, что по умол- чанию значение равно двум, можно попытаться изменить его на четыре. Е В спецификации НА “ГР/1.1 рекомендуется, чтобы приложение-клиент имело не более двух одновременно открытьях соединений с сервером. Как правило число параллельно существующих соединений не должно быть слишком велико, поскольку между преимуществом, получаемым приложением, име- ющим несколько соединений, и накладными расходами, вызванными созданием но- вого соединения, существует тонкий баланс. В некоторый момент приложение, создающее слишком много соединений, будет на самом деле выполняться медлен- нее, чем приложение, благоразумно использующее меньше соединений. Класс WebClient В большинстве Web-приложений копирование данных на сервер и с сервера выполняется повседневно. До появления платформы .NET, чтобы выполнить эту работу, приходилось или покупать компонент независимого изготовителя, или пользоваться WinSock API, и это было довольно утомительно. А вот в библиотеке классов .NET Framework такой компонент появился. Находящийся в пространстве имен System. Net класс WebClient предлагает три метода для копирования данных из удаленного ресурса и четыре метода для передачи необработанных данных или файлов удаленному ресурсу. Для обеспечения доступа к ресурсам Интернета WebClient использует класс WebRequest, поэтому в классе WebClient может использо-. ваться любой зарегистрированный подключаемый протокол. Вообще Говоря, этот класс предоставляет прекрасный способ реализации обмена на HTTP, HTTPS и файловом протоколе. Ниже узнаем, как с использованием этих методов можно пе- редавать данные на сервер и с сервера. Экземпляр класса WebClient можно создать так, как показано ниже: WebCiOit client » new WebClient!У; Метод DownloadDataO Метод DownloadDataO принимает строковое значение, представляющее адрес URL в качестве аргумента, и возвращает массив байтов, полученный по указанному адресу: using System; using System.Nett using System.Text; class WebTest { static vozd Main<) I WebClient client = new WebClient(); byte[] urlData = client.DownloadData(“http://w\<<dot'ietfcrce,com'').; string data = Encoding.ASCII GetString(urlData) ; .. ч Console WriveLine(data);
282 Глава 8 Метод DownloadFileO Метод DownloadFile() принимает в качестве параметров URL-адрес и имя файла. Файл будет скопирован на локальный диск. Пример кода копирует удаленный файл изображения на локальный жесткий диск: using System; using System.Net; using System.IO; class DownloadFile static void Main(string[] args) S { .. . .. . . . ,. string siteURL = “bttp://www.dotnetforce.com/irnages/logo11 .gif”; string fileName = “C: \\ASP.gif’'; // Создаем новый экземпляр WebClient- WebClient client = new WebClientO; // Соединяем имя домена с именем файла Web-pecypca. 5 ' Console.Writetine(“Downloading*JFile \”{0}\" from .......\n\n", fileName, siteURL); // Копируем Web-ресурс и сохраняем era /7 в текущей папке файловой системы, client.DownloadFile(siteURL,fileName); Console.WriteLine(“Successfully Downloaded File from fileName, siteURL); Console.WriteLine(“\nDownloaded file saved in the following ” + “file system folder:\n\t” + fileName); } Метод OpenReadO Метод OpenReadO работает аналогично методу DownloadDataO; единственное от- личие состоит в том, что данный метод возвращает объект St ream, который дает воз- можность считывать данные от целевого' URL. При использовании этого метода данные можно извлекать частями с помощью методов Read() и ReadBlock() объекта Stream. Он позволяет передавать пользователю состояние операции. В следующем примере метод OpenReadO используется для считывания данных, расположенных на удаленном носителе:
НИР 283 using System; using System.10; using System.Net; .> | glass OpenRead static void Main(string[] args) { string siteURL - ’'http://www.recliff .com”; // Создаем новый экземпляр WebClient. WebClient client = new WebClient(); // Объединяем домен с именем файла Web-pecypca. Console.WriteLine(“Start Downloading Data From \”{0}\" ,......\n\n", siteURL); // Копируем Web-pecypc из RemoteURL. Stream stmData * client.OpenRead(siteURL); StreamReader srData * hew StreamReader(stmData); // Создаем файл Fileinfo fiData * new FilelnfoC’C:\\Default.htm”); Streamwriter st = fiData,CreateText(); Console. WriteLine(‘! Writing to the file...”); r. . // Записываем данные в файл < st.WriteLineCsrData. ReadToEnd( )Д; st. CloseO; stmData. CloseO; } } Метод OpenWriteO Метод OpenWrite() используется для отправки данных указанному URL. Это мож- но сделать с помощью метода POST или через другой поддерживаемый метод (на- пример, метод DAV). Метод OpenWriteO принимает строковый параметр адреса и имя применяемого метода HTTP и возвращает поток, в который можно записы- вать данные, чтобы поместить их в указанном URL. В следующем коде файл копируется с жесткого диска на удаленный сервер (пред- полагается, что у нас имеются соответствующие разрешения). Сначала создается объект WebClient, указывающий на ресурс Интернета (например, на страницу ASP.NET), на который хотим отослать данные. Далее преобразуем отсылаемые строковые данные в массив байтов и записываем их в поток, возвращенный мето- дом OpenWriteO объекта WebClient. Поскольку во втором параметре метода OpenW г ite () было задано значение “POST”, данные отсылаются указанной странице: using System; using System.10; using System.Net; - usihg System.Text; class OpenWrite static void Main(strlhg[] args)
284 Глава 8 string siteURL = "http7/localt^ost/postsample.aspx,,; y;c.’ Г !? • V'v. ' г " // Создаем новый экземпляр WebClient. string uploadData = "Posted=True&X=Value"; // Применяем кодирование ASCII для получения массива байтов byte[] uploadArray = Encoding.ASCII.GetBytes(upIoadData); // Создаем новый экземпляр WebCILent WebClient client = new WebClient(), Console.WriteLine(“Uploading data to {0}. ,siteURL); Stream stmUpload = client.OpenWrite(siteURL, “POST”); stmUpload.Write(uploadArray, 0, uploadArray.Length); // Закрываем поток и освобождаем ресурсы. stmUpload.Close(); Console. WnteLine( “Successfully posted the data."), Метод UploadDataO Метод UploadData() отправляет на сервер данные в форме массива байтов без их кодирования. Этот метод принимает в параметрах URL и необязательно метод, используемый для копирования (“POST", ‘GET” и т. д.). В следующем примере приложения с использованием метода POST на сервер ко- пируется строка: using System;1 using System.Net; using System.10; using System,Text; class UploadData ( static void Main(string[] args) { ; .... String siteURL; . , = siteURL = “http://localhost/postsample.aspx”; WebClient client - new WebClient ()'; client.Credentials = System Net CredentialCache OefaultCredentials; string uploadstring = “Hello Force // Добавляем заголовок HTTP Content-Type client.Headers.AddC’Content-Type", "application/x-www-form-urlencoded”); Ц Применяем кодирование ASCII й преобразуем строку в массив байтов, byte[] sendData = Encoding ASCII.GetBytes(uploadstring); Console.WriteLine(“Uploading to {0} siteURL); // Копируем строку методом POST. byte[] recData = client.UploadData(siteURL, “POST0, sendData); // Отображаем ответ. Console.WriteLine(“\nResponse received was {0}", Encoding. ASCI I Get St ring (recData)); \ } f..
НИР 285 Метод UploadFileO Метод UploadFile() работает аналогично методу UploadData(). Он копирует файл (например, с локального жесткого диска) на удаленный ресурс. Метод UploadFile() принимает в параметрах URL имя файла и необязательно используемый метод HTTP. Этот метод копирует указанный файл в заданное место и может возвращать ответ, полученный от целевого URL, в виде массива байтов. В следующем примере файл с локального диска копируется на удаленный ресурс с использованием метода UploadFileO. Заметьте, что с помощью объекта Networkcredential мы указываем информацию учетной записи, поскольку сервер мо- жет не разрешить анонимное копирование. Применяем HTTP-метод PUT, который специально предназначен для копирования файлов на HTTP-сервер. Ответ удален- ного сервера отправляется в массиве байтов, который нужно преобразовать в стро- ку. Для выполнения данной работы используем статическое свойство ASCII класса Encoding из пространства имен System.Text. Этот метод возвращает объект ASCIIEncoding, имеющий метод GetSt ring(), который принимает в параметре массив байтов. Он преобразуется в строку, и та выводится на консоль: using System; using System.Net; using System.IO; using System.Text; class UploadFile / static void Main(string[] args) { string siteURL="http://localhost/images/http.txt*> string remoteResponse; 7 // Создаем новый экземпляр WebClient. WebClient client = new WebClient(); , NetworkCredential cred = new Networkcredential(“username", “password’’., ’’domain”); string fileName = “C.\\http.txt”; \ Console.WriteLine(“Uploading {0} to {1} . fileName, siteURL); // Копируем файл методом PUT. byteQ responseArray = client.UploadFile(siteURL, “PUT”, fileName); // Ответ от целевого URL remoteResponse = Encoding.ASCII GetString(responseArray); Console.WriteLine(remoteResponse!; T } Обратите внимание, что копируемый файл будет отправлен как multipart/ form-data. Кроме самих данных он будет содержать метаданные, поэтому на стороне сервера его нужно будет привести в порядок. Например, если исходный файл содер- жит такой текст: Imagine some text about the HTTP protocol here... * он будет отправлен серверу в следующем формате: ----------------8c410e41d7c7730 Content-Disposition: form-data; name="file"; filename="HTTP.txt"
286 Глава 8 Cpntent-Type: application/octet-stream Imagine some text about the HTTP protocol here... ----------------8c410e41d7c7730 Первую и последнюю строки (границы содержания) можно обнаружить по пара- метру boundary заголовка Content-Туре запроса. Метод UploadValuesO Метод UploadValues() дает возможность скопировать ресурсу Интернета коллек- цию пар имя-значение. Этот метод полезен, когда нуэкно эмулировать запрос POST из HTML-формы и получить ответ. Этим методом значения передаются удаленной цели с использованием коллекции NameValueCollection пространства имен Systpm. Collections.Specialized. Дополнительно можно указать метод отправки значений. Этим методом может быть POST, GET или любой другой поддерживаемый метод. В следующем примере кода выполняется поиск в информации, полученной из Web-страницы SiteContent. aspx. Значения Туре и Keyword передаются этой странице с помощью метода UploadValuesO: using System; using System. Net; using System.10; using System.Text; using System.Collections.Specialized; class UploadValues ' Л static void Main(string[] args) string siteURt = “http:/Aocalhost/Fbrce/SiteContent.aspx”; string remoteResponse; // Создаем новый экземпляр WebClient. WebClient client = new WebClientO; NameValueCollection appendURL = new NameValueCollection(); // Добавляем NameValueCollection appendURL. Add ("Type”, *14" );\ appendURL. Add ("Keyword’1, “WebService”); Console.WriteLine("Uploading the Value pair'’); // Копируем NameValueCollection, используя метод POST byte[] responseArray s client.UploadValues(siteURL, “POST", appendURL); remoteResponse = Encoding.ASCH GetString(responseArray); Console,WriteLine(remoteResponse); ) } Аутентификация В протоколе HTTP предусмотрено несколько простых средств обеспечения бе- зопасности, в частности управление доступом, основанное на удостоверении лич- ности. Классы .NET поддерживают разнообразные механизмы аутентификации клиента, включающие аутентификацию на основе дайджестов, базовую аутентифи- кацию, Kerberos, NTLM и специализированную пользовательскую аутентифика- цию. Чтобы получить эти возможности в свое распоряжение, можно использовать классы CredentialCache или NetworkCredential (принадлежащие пространству имен
НИР 287 System.Net). Оба класса реализуют интерфейс ICredentials, поэтому обычно можно использовать любой класс. Объект Networkcredential сохраняет один набор удостоверений для входа в сеть или обращения к ресурсу Интернета и содержит информацию об имени и пароле для учетной записи пользователя, которую он представляет, и (для систем аутенти- фикации Windows) домене, к которому принадлежит учетная запись пользователя. Класс CredentialCache может сохранять несколько наборов идентификационных удостоверений. Набор удостоверений, действующих для приложения по умолча- нию, можно устанавливать или получать, обращаясь к статическому свойству Ое- faultCredentials класса CredentialCache. Аутентификация достигается установкой объекта Credentials класса HttpWebRe- quest или WebClient перед выполнением запроса. В случае аутентификации на осно- ве дайджестов или базовой аутентификации задаются имя пользователя и пароль. Для NTLM или Kerberos используются средства безопасности Windows, и для объек- TaCredential можно или установить сочетание имени пользователя, пароля и имени домена, или запросить системные значения по умолчанию. Чтобы это понять, са- мое лучшее — рассмотреть код, который будем использовать, чтобы идентифициро- вать себя для ресурса Интернета, требующего регистрации. Более подробно аутентификацию и авторизацию обсудим в главе 11, поэтому сейчас ограничимся самыми простыми моментами Базовая аутентификация Чтобы выполнить запрос к сайту в Интернете, используя базовую аутентифика- цию, надо задать имя пользователя и пароль. Для этого создаем новый объект NetworkC redent ial, передавая ему строки с именем пользователя и паролем и устанав- ливая этот объект в свойстве Credentials объекта HttpWebRequest или WebClient. На- пример, пусть для выполнения запроса используем класс HttpWebRequest: string query = “http://www.rediff.com/’’; WebRequest request = (HttpWebRequest)WebRequest.Create(query); request.Credentials = new NetworkCredential(“Username”» "Password”}; HttpWebResponse response ~ (HttpWebResponse) request. GetResponseQ; ' Y- i . . ; StreamReader reader. - new St reamReader( response. Get ResponseSt reamO, Encoding.ASCII); Г- ' v - * L" • Console. WriteLine( reader. ReadToEnd()); - response. Closed ; > reader.Closed ; Аналогично для объекта WebClient мы указали бы: WebClient client ~ new WebClientO; . % - - Client,Credentials = new NetworkCredential(“U$ername”, "Password”); Аутентификация NTLM В следующем коде запрос к защищенному внутреннему сайту выполняется с ис- пользованием аутентификации NTLM. В данном случае воспользуемся удостовере- нием приложения, установленным пр умолчанию, поэтому зададим для свойства Credentials объектаHttpWebRequest значениеCredentialCache.DefaultCredentials. using System; using System.Net; ' using System.IO; . .. ..... L-X?*' .. 4 XX.X ...... . . .
288 Глава 8 using System.Text; class Credential { ' ... ' static void Main(string!] args) { string query ® “http://www.rediff.com/”; WebRequest request * (HttpWebRequest)WebRequest.Create(query); request. Credentials =• CredentialCache. Defaultcredentials;, HttpWebResponse response » (HttpWebResponse)request. GetResponseO;. * StreamReader reader = new StreamReader(resppnse.GetResponseStream(), Encoding.ASCII); Console. WriteLine (reader.ReadToEnd()); response.Close() ; reader CloseO; } } Аналогично для класса WebClient надо задать: WebClient objWeb = new WebClient!); objWeb. Credentials = CredentialCache.Defaultcredentials; Если бы для регистрации мы захотели использовать разные учетные записи по- льзователя Windows, то могли бы создать новый объект NetworkC redential, указав для учетной записи домен, имя пользователя и пароль: . » request.Credentials = new NetworkCredential (“Username”, "Password*’,“Domain”); Можно добавить удостоверение в объект Credent iaiCache, вызвав его метод Add (). Это дает возможность связать конкретный набор удостоверений с определенным URL Метод Add() принимает три параметра: объект Uri, представляющий префикс URI, для которого должен использоваться этот набор удостоверений, строку, указы- вающую тип аутентификации, и объект NetworkCredential, представляющий учетную запись пользователя, используемую для этого URI: CredentialCache myCreds » new CredentialCache!); NetworkCredential localCred ₽ new NetworkCredential!“Username"', "Password", “Domain”); Uri localUri = new Uri(“http://localhost”); myCreds.Add(locaIUri, “NTLM”, localCred); HttpWebRequest req = (HttpWebRequest)WebRequest.Create( ’ “http://localhost/postinfo. html”)’; req. Credentials = myCreds; Когда устанавливаем в свойстве Credentials объекта HttpWebRequest новый объект CredentialCache, объекты в кэше просматриваются в поиске набора удосто- верений, связанных с URI, к которому хотим обратиться. Первый же набор удосто- верений, соответствующий как URI, так и типу аутентификации, требуемому ресурсом, будет использоваться для регистрации на ресурсе. Поддержка прокси-сервера Поддержкой прокси-сервера протоколом HTTP в классах .NET можно управ- лять на уровне запроса или же ее можно установить один раз глобально на все время существования приложения. Для установки прокси-сервера по умолчанию для всех Web-запросов используем класс GlobalProxySelection:
HTTP 289 h GlobalProxySelection.Select - new WebProxy("proxyserver”, 80);; Объект GlobalProxySelection хранит действующие по умолчанию параметры на- стройки прокси-сервера, которые будут использоваться при доступе к удаленным ресурсам за пределами локальной сети. Параметры настройки прокси-сервера, действующие по умолчанию, получаются из конфигурационного файла приложе- ния. Однако эти параметры могут переопределяться для отдельных запросов. Мож- но также запретить поддержку прокси-сервера, установив в свойстве Proxy объекта HttpWebRequest значение, возвращаемое статическим методом GetEmptyWebProxyO класса GlobalProxySelection. В качестве альтернативы, чтобы установить прокси-сервер для конкретного Web-запроса, создадим объект WebP гоху, используя URI прокси-сервера и порт, на ко- тором он работает. Другие перегруженные конструкторы принимают URI прокси, булево значение, указывающее, нужно ли обходить прокси-сервер для локальных ад- ресов, массив строк других URI, для которых прокси-сервер не следует использо- вать, и объект ICredentials, применяемый для аутентификации на прокси-сервере. using System; 'using System.Net; using System.10; using System.Text; class Proxy static void Main(string[] args) < * WebProxy myProxy « new WebProxy(“proxyserver”, 80) myProxy.BypassProxyOnLopal « true; • string query - "http://www.dotnetforce.com/"; ' HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(query); request.Proxy = myProxy; I } Чтение и запись cookie Протокол HTTP не поддерживает состояние, поэтому в принципе НТТР-серве- ры отвечают на каждый запрос клиента, не привязывая его к предыдущим или по- следующим запросам. Для поддержания состояния между клиентом и сервером нужно программно отслеживать сеанс пользователя, фиксируя информацию о том, к каким ресурсам обращается пользователь и какие данные он вводит. Отслежива- ние сеанса позволяет поддерживать связь между двумя последовательными запроса- ми, выполненными к какому-либо серверу в Интернете. Существуют разнообразные способы поддержания состояния в течение сеанса, в том числе поля <hidden> HTML-форм, cookie и строки запросов URL. Однако cookie, наверное, являются на- иболее распространенными средствами поддержания состояния приложения. Cookie действуют, сохраняя опознавательные знаки на клиенте. Это значит, что управлять созданными файлами cookie должен клиент. Обычно всем этим управля- ет браузер (хотя некоторые браузеры не поддерживают cookie, а пользователи мо- гут отключать их поддержку), но если фронтальное приложение на клиенте не является браузером, то нам нужно самим отслеживать запросы и управлять состоя- нием сеанса. Всякий раз, когда сервер назначает запросу cookie, клиент должен его со- хранить и отправить обратно на сервер со следующим своим запросом. Объекты HttpWebRequest и HttpWebResponse обеспечивают контейнер для хранения cookie, их отправки и получения, но не сохраняют их автоматически, поэтому в нашу задачу
290 Глава 8 входит их сохранение и отправка серверу вместе со следующим передаваемым ему запросом. Для управления cookie используем класс CookieCollection из пространства имен System. Net. Этот класс обеспечивает механизм обработки нескольких cookie. Запись cookie на клиенте Для демонстрации использования cookie в .NET построим пример приложения, который будет создавать cookie, а затем создадим тестовую страницу ASP.NET, что- бы проверить, был ли создан cookie. Для этого потребуется создать виртуальный ка- талог IIS с именем CookieSample. Следующий код (writeCookie. cs) с помощью объекта Cookie устанавливает cookie MyName со значением “Vinod” и отправляет его серверу в HTTP-запросе. За- метьте, что объект Cookie является лишь представлением cookie в памяти — он не со- храняет никаких данных на диске клиента. Когда создаем cookie, мы также устанавливаем путь (URI на сервере, которому этот cookie будет отправляться) и имя домена, для которого cookie действителен. Получив объект Cookie, можно установить его свойство Expires, чтобы указать, когда истечет отведенное для него время (и мы перестанем отправлять его на сервер). Установив эти свойства, добав- ляем cookie в CookieContainer запроса. CookieContainer — это коллекция cookie, кото- рая позволяет хранить cookie для нескольких сайтов. Каждый cookie, добавленный в CookieContainer, включается во внутреннюю коллекцию всех cookie, связанных с конкретным URL using System; using System.Net; usrig System. 10., using System.Text; class WriteCookie { ... 7 - Static void Main(stri.ng[] args) Cookie,Collection cookies = new CookieCdllection^ c ,.,z . //Создание cookie - ; * Cookie cookie - new Cookie(“MyName”, "Vinod", “localbost”); string query * “http://localhost/CookieSample/CookiesTest.aspx"; B HttpWebRequest request = (HttpWebRequest)WebRequest.Create(query}; request CookieContainer » new CookieContainer^); request.CookieContaine r: Add(cookie); • HttpWebResponse response = (HttpWebResponse}request.GetResponseO; StreamReader reader new StreamReader(response.GetResponseStream(), Encoding.ASCII); . > 7 ConsoleWr iteLine (reader. ReadToEndO); reader.Close(j; response.Close() ; I 1 -t - Для тестирования кода создадим очень простую страницу ASP.NET с именем CookiesTest.aspx. Эта страница, которую нужно сохранить в корне виртуального каталога CookieSample, просто получает значение cookie, добавленного нами в WriteCookie. cs. Это значение записывается в поток ответа:
НИР 291 <%@ Page language="C#" %> <% - .; . .v. s. , . . HttpCookie qookie = Request.Cookies[“MyName”]; if (cookie ’= null) Response.Writef‘Value for cookie MyName: ” + cookie.Value); else Response.Write("Cookie not set4); %> , ’ - - 7;. Теперь компилируем и выполняем код WriteCookie. cs. Эта программа выполняет запрос к CooklesTest. aspx и отображает ответ на консоли. На следующем снимке эк- рана показан вывод содержания страницы при выполнении программы. Как можно видеть, значение cookie MyName было получено страницей ASP.NET: Считывание cookie на клиенте Используя пространство имен System. Net среды .NET Framework, считать cookie так же просто, как записать. Чтобы в этом убедиться, опять используем очень про- стую страницу ASP.NET, которая называется WriteCookie. aspx. И теперь нужно по- местить этот файл в корень виртуального каталога CookieSample. Он лишь создает новую запись cookie и отправляет клиенту: Раде 1апдиаде="С#” %> <% - string username = “Vinod”; HttpCookie cookie = i Response.Cookies.Add(cookie)( Response.Write("Hello, " + username);X ,: new HttpCookie("username”, username) fife Запросив эту страницу из клиента, можем обратиться к записи cookie с именем username через свойство Cookies класса HttpWebResponse: string query « ,'http.7/localhost/CookieSample/WrlteCookie.aspxu;. HttpWebRequest req = (HttpWebRequest)WebRequest.Create(query); HttpWebResponse resp = (HttpWebResponse) req.GetResponseO; Console.WriteLine(“Value of Cookie MyName 1 ' 5 resp. Cookies["MyName”].Value): Значение cookie можно прочитать, как только через вызов метода GetResponseO получены заголовки запроса. Заметьте, что коллекция Cookies объекта HttpWebResponse будет заполнена, толь- ко если было установлено свойство CookieContainer соответствующего HttpWeb- Request. Иначе нужно считывать cookie из коллекции Headers. Записи cookie отправляются в заголовке Set-Cookie приблизительно в таком формате: Set-Cookie: FirstName=Vinod; path=/.LastName=Kumar; path=/
292 Глава 8 Таким образом, можем получить значение нужной записи cookie. Для этого нуж- но найти ее имя в заголовке, определить положение следующего знака равенства и следующей точки с запятой и, наконец, извлечь подстроку между этими двумя сим- волами: string cookie - response.Headerst“Set-Cookie"]; if (cookie != null) int start - cookie.IndexOf(“FirstName”); int equals = cookie.IndexOf(*=’, start); int end « cookie.IndexOf(‘;?, equals); if (equals t= -1 && end ! - -1) v .string value * cookie. Subst ring (equals + 1, end - equals - 1); » sSL... ‘'ч. ~ * г V : ‘ ‘ Поддержание состояния с помощью cookie Простейший способ поддерживать состояние на клиенте состоит в сохранении отправленных сервером cookie в коллекции CookieCollection. Рассмотрим на двух последовательных запросах, как это работает. Сначала вызовем страницу WriteCookie. aspx, в которой в cookie MyName устанавливается значение “Vinod”. Полу- чив ответ на этот запрос, сохраним записи cookie из ответа в коллекции Cookie- Collection. Затем создадим новый объект HttpWebRequest и добавим объект CookieCollection в его свойство CookieContainer. Потом отправим запрос странице CookiesTest. aspx, которая прочитает значенйе cookie MyName и запишет его в поток ответа: using System; using System.Net; using System.10; using System.Text; class CookiePersist • static void Main() < // Делаем первый запрос (к WriteCookie.aspx) ? string query « Mbttp //localhost/CookieSample/WriteCookie.aspx”; HttpWebRequest request = (HttpWebRequest)WebRequest.Create(query); // Устанавливаем для запроса CookieContainer, иначе he сможем // считать cookie из заголовка ответа. request. CookieContainer *= new CookieContainerO; // Отправляем запрос и получаем ответ HttpWebResponse response = (HttpWebResponse)request.GetResponse(-); 1 // Сохраняем cookie из ответа в объекте CookieCollection CookieCollection cookies = response.Cookies; // Отображаем тело ответа в окне консоли < « StreamReader reader = new StreamReader(response.GetResponseStream()>; Console.WriteLine(reader.ReadToEnd()); reader.Closed; • , .. - response. CloseO; // Создаем второй запрос <k CookiesTest.aspx)
НИР 293 HttpWebRequest nextRequest - (HttpWebRequest)WebRequest.Create( ,f. i;1 “ http-.//local host/CookleSample/CookiesTest. aspx") .// Добавляем сохраненный CqpkieCollection к CookieContainer запроса nextRequest.CookieContaineг « new CookieContainer(); ” nextRequest.CookieContainer.Add(cookies); // Отправляем запрос и выводам ответ; HttpWebResponse nextResponse = (HttpWebResponse) < • nextRequest.GetResponse(); reader = new StreamReader(nextResponse.GetResponseStreamO); Console WriteLine(reader. ReadToEndO); reader.Close(); nextResponse.CloseO; } ' «У' Запустив эту программу, увидим, что значение, установленное для cookie MyName в странице WriteCookie. aspx, было передано странице CookiesTest. aspx: > HTTP-сервер с поддержкой ASP.NET Одна из самых замечательных особенностей технологии ASP.NET состоит в ее способности выполняться вне ITS. Если говорить точнее, она поддерживает среду хостинга (в пространстве имен System. Web. Hosting), которая дает возможность со- здать при поддержке ASP.NET собственный Web-сервер. В этом разделе построим HTTP-сервер, который может обрабатывать страницы ASP.NET. И назовем мы его WroxServer. Конфигурационные файлы сервера Прежде чем приступим к написанию кода, рассмотрим XML-файлы, в которых будет храниться конфигурационная информация для нашего сервера. Таких фай- лов три: □ Hostinfo.xml □ Default.xml □ Mime.xml В файле Host Inf о. xml будет храниться информация хостинга для сервера— вирту- альный путь и номер порта. Используем порт 8001, но можно выбрать любой сво- бодный порт: <HostLocation> <VDi r>C:\\WroxServer</VDi r>
294 Глава 8 <Port>8001</Port> </HostLocation> Файл Default. xml содержит имена документов по умолчанию. Эти документы сер- вер будет искать в виртуальном каталоге, если в запросе браузера задано не точное имя документа, а только каталог. Документом по умолчанию может быть начальная страница каталога или индексная страница, в которой перечисляются документы этого каталога. <Document> <File>default.htm</File> <File>default.aspx</File> <File>Index.htm</File> <File>Home.aspx</File> </Document> Файл Mime, xml содержит информацию о различных MIME-типах, поддерживае- мых сервером, и расширения файлов, связанные с каждым типом. Эта информация позволит серверу корректно устанавливать заголовок ContentType для каждого типа: <Mime> <Values> <Ext>.htm</Ext> <Type>text/html</Type> </Values> ' <Values> <Ext>.html</Ext> 4 <Type>text/html</Type> </Values> <Values> <Ext>.aspx</Ext> <Type>text/html</Type> . </Values> - . <Values> <Ext>.gif</Ext> <Type>image/gif</Type> </Values> z <Values> <Ext>.jpg</Ext> <Type>image/j pg</Type> </Values> </Mime> Чтобы было удобнее извлекать эту информацию, сохраняем ее в формате XML, но можем использовать также текстовые файлы, реестр и другие способы. Кодирование сервера Теперь посмотрим, как работает наш НТ ГР-сервер. Клиент инициирует HTTP-запрос, открывая сокет TCP/IP с портом Web-сервера (8001 для нашего сер- вера) и отправляя следующий запрос в коде ASCII: GET /Server.html НТТР/1.1 Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms- powerpoint, application/vnd.ms-excel, application/msword, */* Accept-Language: en-us Accept-Encoding: gzip, deflate
НИР 295 User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.2914) Host: localhost:8001 Connection: Keep-Alive Получив этот запрос, сервер записывает в сокет копию запрошенного ресурса, откуда ее считывает клиент, и затем закрывает соединение. Как только соединение закрывается, сервер забывает все, что связано с этим запросом. Начнем с создания в Visual Studio .NET нового проекта консольного приложе- ния Visual C# и дадим ему имя Htt pSe rve г. Этот проект содержит три класса: WroxSe г- ve г, где реализуем HTTP-сервер, Host и ASPXHost ing, которые будем использовать для обработки любых запрошенных страниц ASP.NET. Добавим в наш проект ссылку на System.Web.dll. Класс WroxServer Начнем с импортирования пространств имен, требуемых для приложения и определения пространства имен HttpServer. Первый класс в этом пространстве имен — WroxServer. В нем есть два поля: открытое перечисление, которое будет ис- пользоваться для указания той части конфигурации, которую хотим считать из фай- ла Hostinfo, xml, и объект TcpListener, который будет слушать запросы от клиентов. В конструкторе класса WroxServer просто запускаем TcpListener на порту, полу- ченном из конфигурационного файла Hostlnf о. xml. Затем создаем новый потоки вы- зываем на нем метод StartListen(). Этот метод будет принимать любые запросы от клиентов. using System; using System.10; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading ; using System.Web; using System.Web.Hosting ; using System.Xml; namespace HttpServer { A class WroxServer : MarshalByRefObject // перечисление для Hostinfo public enum Hostinfo { VirtualDirectory, Port } private TcpListener myListener; . s > // Конструктор, запускающий TcpListener на данном порту. //* Он также вызывает поток на методе StartListen(l. public WroxServerO ’ try « • { // Начинаем слушать на данном порту myListener = new TcpListpner(Int32.Parse(GetHostingInfo( HttpServer WroxServer.Hostinfo.Port))); myListener. StartO; - 7 . Console.WriteLine(“Web Server Running...,;Press “C to Stop,./’); .1 « Ч"'-. 4 // Начинаем, поток, вызывающий метод StartListenf'. Thread thread = new Thread(new ThreadStart(Starttisten)); thread.Start<); 1 » • г"- лЯъ. is» - & ж-; V * ' 4
296 Глава 8 catch (NullReferenceException) I < // Даже не спрашивайте, зачем они порождают это исключение, // когда встречают нулевую ссылку Console,Write,Line( "Accept failed. Another process might be ” + ..i “bound to port ” + HttpSe rver. WroxSe rver.HostInfо.Port,ToSt ring()); Чтение из конфигурационных файлов Мы получаем информацию о порте из файла Hostlnfo.xml в методе GetHostingInfo(). Этот метод принимает в качестве параметра значение Host Info и возвращает или виртуальный каталог, или номер порта в зависимости от пара- метра: public string GetHostingInfo(Hostinfo InfoType) ».• £ string retVal - string xPath = try r / • г : /7 Установим выражение XPath для поиска узла VDir или Port // в зависимости от параметра, переданного в методе if (InfоТуре.Equals(HostInfо .Vi rtualDi recto ry)) xPath a "HostLocation/VDlr”; else if (InfoType.Equals!Hostinfo.Port)) xPath - "Hosttocation/Port”; else return tl Загрузка XML-файла XmlDataDocument xDHost = new XmlDataDocumentO; xDHost.Load("data\\HostInfo.xml”); У/ выбираем соответствующий узел XmlNode node = xDHost. SelectSingleNode(xPath).; // Получаем текстовое значение элемента retVal = node.InnerText.TrimO; } Л ’ catch(XmlException eXML) Console.WriteLine(“An ConflgFile Exception Occur red.;/" + eXML.ToStringO); return retVal; } Если пользователь не предоставил имя файла, метод GetTheDefaultFileName() по- лучает из файла default. xml имя файла по умолчанию. Этот метод принимает путь каталога и ищет файл в этом каталоге. Если найден какой-либо из файлов по умолча- нию, он возвращает это имя, иначе возвращает пустую строку: public string GetTheDefaultFileName( string sLocalDirectory) string sLine = { // Загружаем XML-документ
НИР 297 XmlDataDocument xDFile = new XmlDataDocument(); xDFile.Load(“data\\Default.xmln); // Выбираем все элементы <File> XmlNodeList fileNodes = xDFile. SelectNodes(’*Document/File”); // Выполняем цикл по выбранным узлам, пока не найдем , // один из файлов по умолчанию foreach(XmlNode node in fileNodes), if (File.Exists(sLocalDirectory + node.InnerText.Trim())) sLine = node.InnerText.Trim(); break; } } } catch(XmlException eXML) { / . - > \ , Console.WriteLine(“An ConfigFile Exception Occurred t " + eXML. ToStringO); } - .. v . // Возвращаем имя файла, если существует файл по умолчанию, . // иначе возвращаем пустую строку г . := if (File.Existsf sLocalDirectory + sLine))\ return sLine; else return } Следующий метод, GetMimeTypeO, используется для идентификации MIME-типа по расширению файла, запрошенного клиентом. Этот метод принимает во входном параметре имя файла и проверяет его расширение по информации о MIME-типах в файле Mime. xml. Он возвращает соответствующий М1МЕ-тип: х public string GetMimeType(string sRequestedFile) { ’ - ’ ' - - string sMimeType - string sFiieExt ~ string sMimeExt = // Преобразуем в строчные буквы sRequestedFile = sRequestedFile. ToLowerO; int iStartPos « sRequestedFile.IndexOf(“. *•- sFiieExt = sRequestedFile.Substring(iStartPos); try I // Загружаем файл Mime.xml для определения Mime-типа XmlDataDocument xDMime - new XmlDataDocumentO;’ xDMime.Load(“data\\Mime.xml”); // Выбираем элемент <Type>, имеющий родственный элемент <Ext> // с тем же значением, что и расширение нашего файла string xPath = “Mime/Values/Type{. ./Ext=’” + sFiieExt XmlNode mimeNode = xDMime.SelectSingleNQdd(xPatfr); if (mimeNode й null) ? <SMimeType * mimeNode.InnerText.Trim(); // Получаем значение предыдущего элемента <Ext>
298 Глава 8 sMimeExt = mimeNode. PreviousSibling. InnerText.TrimO; } J?* ' , £ J * catch (Exception e) ' -* Console.WriteLineCAn Exception Occurred : ” + e.ToStringO); if (sMimeExt == sFileExt) V. return sMimeType; else return } Отправка данных клиенту Ha этом закончим набор методов для чтения конфигурационных файлов и рас- смотрим метод WriteHeaderO, который используется для отправки информации HTTP-заголовка браузеру. Этот метод принимает в качестве параметров информа- цию, необходимую для построения заголовков, в том числе версию HTTP, тип со- держа ия и длину содержания. Он также принимает в параметре ссылку на объект Socket. Построив заголовки, отправляем их клиенту, используя этот сокет: public void WriteHeader(string sHttpVersioni string sMIMEHeader, int ITotalBytes, string sStatusCode, ref Socket mySocket) string sBuffer ж ; // Если Mime-тип не предоставлен, устанавливаем по умолчанию тип text/html if (sMIMEHeader.Length =- 0) sMIMEHeader * “text/html”; sBuffer = sBuffer + sHttpVersion + sStatusCode + “\‘r\n’-; sBuffer = sBuffer + "Server: WroxServer\r\n”; sBuffer = sBuffer + “Content-Type:'” + sMIMEHeader + “\r\n"; sBuffer =• sBuffer + “Accept-Ranges: bytes\r\n"; . sBuffer « sBuffer + “Content-Length: ” + iTotalBytes. + "\r\n\r\n#>; bytef] bSendData = Encoding. ASCII. GetBytes( sBuffer):; SendToBrowser(bSendData, ref mySocket); Console. Write tine (“Total Bytes1*: ’’ + ITotalBytes. ToStringO); } SendToBrowser() — это перегруженный метод, отправляющий информацию кли- енту. Он принимает в параметрах или строку, или массив байтов и ссылку на сокет и отправляет эту информацию клиенту. HTML-код, сгенерированный, после того как файл ASP.NET обработан с использованием класса ASPXHosting, передается как строковый параметр и преобразуется в массив байтов с помощью класса Sys- tem. Text. Encoding. Затем он передается другому перегруженному методу SendToBrowser (): public void SendToBrowser(string data, ref Sgcket socket) SendToBrowser(Encoding.ASCII.GetBytes(data), ref socket); ал Другой перегруженный метод SendToBrowser() просто отправляет массив байтов клиенту, вызывая метод Send() переданного ему класса Socket. Если он возвращает -1, мы понимаем, что произошла ошибка: •
НИР 299 public void SendToBrowser(Byte[] bSendData, ref Socket socket) . ... • • < Int INumByte = 0; • try , ' . { .« «Й» if (socket.Connected) < .. if (( iNumByte = socket.Send(bSendData, bSendData.Length,0)) == -1) Console. Writ eLine (“Socket error: cannot send packet’’); else Console.WriteLine(“No. of bytes send {0}” , iNumByte); } else Console.WriteLine(“Connection Dropped_____”); } catch (Exception e) j { Console.WriteLineC“Error Occurred : {0} ”, e ); Прием соединений Методы, с которыми мы до сих пор знакомились, являются компоновочными блоками приложения. Теперь рассмотрим метод StartListen(). Этот ключевой ме- тод дает согласие на установление соединения между клиентом и сервером, а также обрабатывает запрос от клиента и отправляет ему ответ на основании запроса: public void StartListenQ int iStartPos ~ 0; string sRequest; * string sDirName; . string sRequestedFile; ; 1 r? string sErrorMessage; string sLocalDir; // Получаем информацию о virtualDiг string sWebServerRoot - GetHostinglnfof < HttpServer WroxServer.Hostlnfo.VirtualDirectory); string sPhysicalFilePath = string sFormattedMessage = string sResponse = while(true) { // Принимаем новое соединение Socket socket = my Listener .AcceptSocketO; Console.WriteLine (“Socket Type ” + socket.SocketType ); if(socket.Connected) • Console.WriteLine(“\nCIient Connected!!\n" + “====^==========?^\nCLient IP {0}\n”, socket.RemqteEndPoint) £ // Make a byte array and receive data, from the client J ; byteU bfieceive * new byte[ 1024] int i = socket.Receive(bReceive,bReceive.Length,0) ;
300 Глава 8 ' // Преобразуем байты в строку string sBuffer = Encoding.ASCII.GetString(bReceive); // Просто убедимся, что мы используем Н1ГР, // больше нам не о чем беспокоиться IStartPos * sBuffer ]ndexOf(‘HTTP', 1); |// Получаем текст "HTTP” и версию, например, “НТТР/1.1” string sHttpVersion = sBuffer.Substring(iStartPos, 8); к, sRequest = sBuffer.Substring(O, IStartPos - 1); // Заменяем обратные слэши, если такие есть, прямыми ' sRequest.Replace(“\\”, // Если имя файла не представлено добавляем прямой слэш, чтобы 7/ указать, что это каталог а затем поищем ’ // имя файла, используемого по умолчанию... if ((SRequest. IndexOf(”i”) <1) && (! sRequest. EndsWith("/”;))) sRequest ~ sRequest * "/”:; // Извлекаем имя запрошенного файла iStartPos = sRequest IcstifidexOfC/”) + 1; sRequestedFile = sRequest.Substring(iStartPos); - // Извлекаем имя каталога sPirName = sRequest.Substring(sRequest.IndexOf("/”), sRequest.LastIndex0r(‘7”)-3); // Определяем; физический каталог if (sDirName == s Local Dir k sWebServerRoot; else { // Получаем виртуальный каталог sDi rName =sDi rName. Replaced"/’',); sLocalDir = sWebServerRoot + sDirName; Console WriteLine(“Directory Requested + sLocalDir); Приведенный код более или менее очевиден. Как уже упоминалось, он принима- ет соединение, получает запрос и преобразует его из массива байтов в строку. Затем он ищет тип запроса, извлекает информацию о версии HTTP, а также о файле и ка- талоге и получает информацию о виртуальном каталоге из файла Hostlnfo.xml, ис- пользуя метод GetHostingInfo(). Если файл не задан и мы не можем найти файл, используемый по умолчанию, то отправляем браузеру ошибку HTTP 404 Not Found: •¥ // Определяем имя файла. Если имя файла не предоставлено, // ищем в списке файлов по умолчанию j if (sRequestedFile.Length 0) • * ; * { < ' ‘ ;.............................* * * * ; ; « -r // Получаем мня файла, исполойуеМЬ| о по умолчаний sRequestedFile = GetTfeDef aultFileName(sl ocalDir); if (sRequestedFile -° ’•”) г- _ 'ir (S' i’I,..; i- . ’ ... .• 1 t sErrorMessagt: ? ’’«H^Errorl} flo Default File Nane + ’ V ’ » specif ied</H2>*; >’ - WriteHeader(SH1temersion, SErrorMessage.Length, “ 40'1 Not Found”, ref socket); ... SendToBrowser ( sEerdrUessage, ref socket);
HTTP 301 socket.Close(); return; t' Аналогично следующий код получает MIME-тип из XML-файла Mime. xml, исполь- зуя метод GetMimeType(), и затем проверяет расширение файла. Если указано расши- рение .aspx, то будет создан экземпляр класса ASPXHosting, и вывод HTML будет передан в параметре методу SendToBrowser() (вместе с ссылкой на сокет). Вывод ге- нерируется методом CreateHost(), принимающим в параметре имя файла ASP.NET. Если расширение запрошенного файла не .aspx, то он считывается методом Binary-Reader() и отправляется браузеру методом SendToBrowser(). В обоих случаях перед методом SendToBrowserO вызывается метод SendHeader(). После отправки дан- ных клиенту соединение закрывается: // Получаем Mime-гтип string sMjmeTypQ = GetMlmeType(SRequestedFile); // Строим физический путь sPhysicalFilePath ~ sLocalDir + "\\” + sRequestedFile; Console.WгiteLine(“File Requested : ” + sPhysicalFilePath); if (File.Exists(sPhysrcalFilePath) == false) sErrorMessage s “<H2>404 Errorl File Does Not Exists...</H2>”; WriteHeader(sHttpVersiont, f”., sErrorMessage.Length, “ 404 Not Found”, ref socket); SendToBrowserfsErrorMessage, ref socket); Console.WriteLine(sFormattedMessage); else jb . ...Л string ucReqFile - sRequestedFile. ToUpperO; . .. ; Л ; // Если запрошенный файл - страница ASP.NET if (ucReqFile.Subst ring!ucReqFile,Length-4).Equals!“ASPX”)) // Создаем экземпляр класса ASPXHosting ASPXHosting host = new ASPXHosting!); // Передаем файл ASPX, чтобы получить вывод HTML string HTMLOut = host. CreateHost! sRequestedFile); WriteHeader(sHttpVersion, sMimeType, HTMLOut.Length, “ 200 OK”, ref socket); SendToBrowser!HTMLOut, ref socket); ) else { int iTotBytes=Q; sResponse - ; FileStream fs =- new FileStream!sPhysicalFilePath, FileMode.Open, FileAccess.Read, FileShare.Head); // Создаем reader, считывающий массив байтов // из FileStream BinaryReader reader = new BinaryReader(fs); byte(] bytes « new byte[fs,Length]; a int read; , -z -- ' * * ’ while ((read = reader.Read! ; л a ’ s . . -bytes, C bytes^Length)). 1» 0)
302 Глава 8 // Читаем файл и залю^ваем данные в :еть ^Response = sReoponse + Encoding ASCII.GelStr/ngC > bytes, 0, read)? iTotBytes = iTotBytes + read; ’ •'= • - j " * ?< ' leader.Closed; fs. CloseO; WriteHeader(sHttpVersion, sMimeTyoe. iTotBytes, "200 OK”, ref socket); SendToBrowser(bytes, ref socket); } } -s socket. Closed; Наконец, чтобы запустить сервер, нужен метод Maiп(): static void MainO WroxServer server = new WroxServer (); } Теперь рассмотрим классы Host и ASPXHosting, которые будут обрабатывать лю- бые запросы к файлам ASP.NET. Размещение приложений ASP.NET вне IIS Как уже упоминалось, одной из наиболее интересных характеристик ASP.NET является способность выполнения вне IIS. Точнее, в ASP.NET поддерживается среда хостинга (в пространстве имен System. Web. Hosting), позволяющая запускать приложения поверх других Web-серверов. Для этого пользуемся классами SimpleWorkerRequest и ApplicationHost. В последнем из этих классов есть метод CreateApplicationHost (), который применяется для создания экземпляра класса Host в домене приложения, используемого для размещения ASP.NET. Этот класс позво- ляет выполнять маршаллинг вызовов методов между AppDomain нашего Web-сервера и App Domain ASP.NET. (Маршаллинг—передача параметров и возврат результатов при передаче вызова в другое адресное пространство. — Прим. науч, ред.) Класс SimpleWorkerRequest представляет собой несложную, предварительно определенную реализацию абстрактного класса HttpWorkerRequest, который предо- ставляет методы и перечисления, используемые ASP.NET для обработки запросов. Чтобы обработать страницу ASP.NET, нужно передать объект HttpWorkerRequest в метод ProcessRequest() класса HttpRuntime (из пространства имен System. Web), поэ- тому использование класса SimpleWorkerRequest освобождает от необходимости со- здавать собственную реализацию: using System; using System.10; using. System.Web; using System.Web. Hosting; using System. Xml;, public class Host : MarshalByRefObject { < public string HandleRequesc(slring filename) { \ Stringwriter wr = new StringWrjter();
НИР 303 Console.Wr±teLine(“The output f'romlihe {0} fils", fileName); •• ’ A 4 -/г ' ; i аз- 'i*l . К* 'Д //Создаем Worker для выполнения файла aspx - ч. ’ 1 HttpWorkerRequest worker = new SimpleWorkerRequest(fileName, . wr); ♦. ® Л j= У1 J // Выполняем страницу ... ~ 4 HttpRuntime.ProcessRequest(worker) ; •' return wr.ToStringO; J } 1 public Class ASPXHosting < public ерит HostInfo{VirtualDirectory, Port} public string CreateHost(string fileName) { >. ' ' ;; ,. . - Host myHost - (Host)ApplicationHost.CreateApplicationHost( typeof(Host), *7”, GetHostingInfo( ASPXHosting.Hostinfo VirtualOirectory)); return myHost.HandleRequest(fileName); , w public string GetHostinglnfо(HostInfo InfoType) { .• ’ •> // Как в классе WroxServer - у // мы не будем повторять здесь тот же код } , > - . •• • . - • ; I Чтобы протестировать этот код, организуйте сборку в глобальном кэше сборок, загляните в главу 8 книги “Professional C# 2nd Edition” (Wrox Press, ISBN 1-86100-704-3). Теперь выполним страницу ASP.NET с именем WroxServerSample. aspx. Эта стра- ница помещается в корневой каталог Web-сервера. Поскольку мы выбрали порт 8001, то для вызова файла нужно задавать URL как http://localhost:8001/WroxSer- verSample.aspx. Этот файл содержит Web-элемент управления Label с некоторой информацией: <%@ Page language^’с#” ' <!DOCTYPE HTML PUBLIC <с-//W3C//DTD HTML 4.0 Transitional//EN” > .. <html> <head> <title>WroxServe rSample</title> J </head> <body MS_POSITIONING="GridLaycut’> <form id="WroxServerSample” method- post”' runat=”server"> <asp: Label id='LabelT* Funat=”server'’ ForeColor="#COOOOO" Font-Bold^"True" Font-Names="verdana*> WroxServerSample.aspx page Processed using Wrox Serverc/asp.Label> </form> </body> </htmI> 9? Ж На приведенном ниже рисунке показан вывод из файла WroxServerSample. aspx в Web-браузере. Этот пример демонстрирует мощь классов .NET, а также простую и ясную под- держку, предоставляемую этой платформой для большинства средств протокола Н^ТР/1.1.
304 Глава 8 HTTP и .NET Remoting В главе 5 были представлены основные понятия .NET Remoting и рассказано, как можно использовать эту среду с каналом TCP. Для установления соединений - между приложениями среда .NET Remoting Framework использует каналы. В настоя- щий момент в .NET Framework доступны два канала: TcpChannel и HttpChannel. Как правило, если нужно отправить сообщение по локальной сети, должен использо- ваться канал TCP, имеющий более высокую производительность, а если сообщение отправляется через Интернет, то более удачным выбором следует признать канал HTTP, поскольку TCP-соединения не разрешается устанавливать через брандмауэр. По умолчанию сообщения отправляются по каналу HTTP и форматируются с ис- пользованием SOAP. Этот канал удобен для интероперабельности, но уступает в производительности двоичному каналу, обычно используемому по умолчанию с TCP. Как и в случае канала TCP, прежде чем использовать канал HTTP в наших прило- жениях, нужно зарегистрировать его с канальными сервисами: public class LoaJHttpChannel 'HttpChannel hltpChanneH.. "л^ public i/oid loadChannelO < . . < • ... .1 httpChannel = new HttpChannel 0; : . , // Регистрируем канал НИР \ Channelservices. RegisterCharinel(httpChannel); 1 ; g Л? П' e . * ..................................................... > Построение простого приложения для среды Remoting Поскольку использование Remoting с каналом HTTP концептуально очень схо- же с использованием канала TCP, не будем заново излагать теорию, а сразу же пе- рейдем к рассмотрению небольшого примера приложения. Сначала нужно определить класс, экземпляр которого создадим на удаленной машине. Сделаем его как можно более простым и определим лишь один метод, кото-
HTTP 305 рый выводит на консоль строку, переданную в параметре. Этот класс должен сущес- твовать как на клиенте, так и на удаленном сервере, поскольку при вызовах методов на удаленном объекте среда Remoting использует отражение. // Wrox Log cs , ’ using; System; ' ’ / I* tfe- . V-. ' namespace WroxLog : ... - " - * >v?‘ public class RemotingSample ; MarshalByRefObject public void RemoteLpg(string value) v.;'W T Console. Writeline (value); ' ..) 'X I Откомпилируйте этот файл в DLL командой esc /t: library WroxLog. cs. Чтобы сделать этот класс доступным на удаленной машине, надо наследовать его от класса MarshalByRefObject. Если объект допускает маршаллинг по ссылке (Mar- shaled By Reference, MBR), то ссылка на него передается из одного AppDomain в другой. Иначе, если объект допускает маршаллинг по значению, целевому Арр- DoL rain передается копия полного состояния объекта. Удаленное приложение ис- пользует метаданные, чтобы создать “прокси” для исходного объекта. Можно вызывать этот прокси, и тогда вызовы будут передаваться маршаллингом реальному объекту. Теперь создадим простое серверное приложение, слушающее запросы: // RemoteServer.cs using System; using WroxLog; using System,Runtime.Remoting; using System.Runtime. Remoting. Channels; ,, -using System.Runtime.Remoting.Channels.Http; namespace RemoteServer class RemotingServer { i > public static void Main() { Channelservices.Registerchannel (new HttpChannel (8000) ) ; RemotingConf xguration.Regi ste rWel1KnownSe rvice Гуре( typeoffRemotingSample), “WroxLog", . WellKnownObjectMode.Singleton); « t- Console.WnteLineC'Log Server Listening on enopoints \r\n” + “\thttp://localhost:8000/WroxLog\r\n"); Console.WriteLine(“Press enter to stop the server...”); 1 •’ '4 Console. ReadLineO; ) } } Эту программу нужно откомпилировать в консольное приложение, подключив ссылку на сборку WroxLog. dll: esc RemoteServer.es /riWroxLog.dll
306 Глава 8 В приведенном коде регистрируем канал HTTP, чтобы использовать его как транспортный механизм и слушать на порте 8000. После регистрации канала мето- дом RegisterWellKnownServiceType() регистрируем объект, экземпляр которого будет фактически создаваться на удаленном сервере. Этот метод сообщает инфраструкту- ре Remoting, где найти удаленный объект. В параметрах передаем тип объекта, URI и WellKnownObjectMode. Здесь мы используем значение Singleton, чтобы один экзем- пляр объекта использовался для каждого входящего сообщения. Когда вызов поступает на сервер, .NET Framework извлекает из сообщения URI, исследует таблицы Remoting в поисках ссылки на объект, соответствующий этому URI, и затем при необходимости создает экземпляр объекта, направляя ему вызов метода. Если объект регистрируется как SingleCall, он не будет повторно использо- ваться после завершения метода, а остается для уничтожения “сборщику мусора”; поэтому для каждого вызова метода создается новый экземпляр объекта. Экземпляр самого удаленного объекта не создается процессом регистрации. Это происходит, лишь когда клиент пытается вызвать метод на объекте или активизирует объект со стороны клиента. Далее показан код клиента, выполняющий обращение к этому серверному при- ложению: // RemoteClient cs using System; using System.Runtime.Remoting; using WroxLog: namespace RemotingClient { - ' Л. class Client static void Main() RemotingSample httpWroxLog - (RemotingSample)Activator.GetObj£ct( typeof(RemotingSample), “http://localhost:8000/Wroxl og”); httpWroxLog.RemoteLogC"Client ; Hello..Server"); И этот код нужно откомпилировать в консольное приложение, указав ссылку на нашу сборку WroxLog. dll: esc RemoteClient.es /r:WroxLog.dll В этом коде объект RemotingSample создается с использованием модели активиза- ции сеовером, через вызов метода Activator. GetObject(). Объекты, активизируе- мые сервером, — это объекты, время существования которых напрямую контролируется сервером. Домен серверного приложения создает такой объект, только когда клиент вызывает метод на этом объекте, а не при первом создании кли- ентом экземпляра объекта через ключевое слово new или Activator. GetDbj ect () (ана- логично позднему связыванию). Такой подход позволяет сэкономить цикл сетевого обмена, нужный только в целях создания экземпляра объекта. Когда клиент запра- шивает экземпляр типа, активизируемого сервером, в домене приложения-клиента создается только прокси-объект. На следующем снимке экрана отображается вывод на стороне сервера, получен- ный, когда мы запускаем сервер и затем — приложение-клиент:
HTTP 307 Итоги В этой главе было подробно рассмотрено программирование по протоколу HTTP. Первое, что требуется в интернет-программировании,—это хорошее знание принципов работы HTTP, поэтому начали эту главу с обзора протокола HTTP, а по- том рассмотрели поддержку программирования HTTP в .NET. В этой главе мы в основном разобрали следующие темы: Обзор протокола HTTP — основные понятия, свойства и характеристики протокола, запросы и ответы Использование методов и классов .NET для работы с протоколом H'IT'P Считывание и запись cookie в .NET Создание сервера и клиента HTTP в .NET и хостинг приложений ASP.NET вне IIS Использование канала HTTP с .NET Remoting □ □ □ )
ГЛАВА 9 Протоколы электронной почты главе 1 рассматривались разнообразные протоколы, предназначенные для отправки и получения сообщений электронной почты. Протоколы электронной почты весьма широки в том смысле, что имеют отношение не только к платформе .NET. Даже с развитием средств обмена сообщениями в Интернете электронная почта остается основой электронного обмена. Я уверен, что почти каждый чита- тель этой книги уже имеет некоторый опыт отправки электронной почты из разра- ботанных им программ, поэтому на самом деле мы вовсе не обращаемся к какой-то новой и увлекательной технологии, впервые представленной исключительно плат- формой .NET. Но поражает то, каким легким и цельным становится сетевое про- граммирование в .NET, особенно по отношению к использованию разнообразных протоколов электронной почты для отправки и получения сообщений и выполне- ния мг огочисленных задач, реализовать которые в прошлом было совсем не про- сто. В этой главе представлен высокоуровневый обзор разнообразных протоколов электронной почты и анализ возможности их использования в среде .NET. Приве- дем конкретные примеры на C# и обсудим каждый из них. Об электронной почте коротко Электронную почту легко себе представить как обычную почту, которой все пользуются. Самое серьезное отличие состоит в том, что на электронные письма не нужно наклеивать марки! Ну и еще, конечно, доставка электронного письма выпол- няется почти мгновенно, в то время как почтовая служба работает очень медленно. Отправка электронной почты приносит и другие преимущества. Электронное пись- мо можно отправить сразу нескольким получателям и также легко перенаправить от одного адресата другому. Для обоих случаев сеанс передачи письма имеет началь- ную и конечную точки. Существует также механизм доставки, который передает почту из одной точки в другую по почтовому маршруту, пока письмо не достигнет пункта назначения. В этом сценарии взаимодействуют два компонента:
Протоколы электронной почты 309 □ Агент передачи сообщений (Message Transfer Agent, иди МТА) □ Агент пользователя (клиент электронной почты) В примере с “тихоходной” почтой из реального мира агент передачи может быть почтовой или транспортной компанией, такой, как USPS (United States Postal Service), FedEx или UPS. Отправитель и получатель — очевидны. В электронном мире место агента передачи занимает реализация протокола SMTP (Simple Mail Transfer Protocol), который должен переправлять почтовые сообщения от началь- ной точки (отправителя) до конечной точки (получателя) через самые разные ма- шины в Интернете. Имейте в виду, что этот метод уходит своими корнями в TCP/IP и в настоящее время наиболее распространен при передаче сообщений. Однако в прошлом для электронной почты использовались другие системы и стандарты. Как работает электронная почта Рассматривая реальную отправку сообщений электронной почты через Интер- нет, в первую очередь надо осознать, что Интернет — это не что иное, как сотни ты- сяч компьютеров, соединенных в единую сеть с использованием одних и тех же стандартных протоколов обмена. Как это вписывается в ситуацию с электронной почтой? Конечные точки — это простые почтовые ящики или хранилища сообще- ний на серверах электронной почты (хотя можно представить отправителя и полу- чателя как начальную и конечную точки, а хранилища сообщений как места, где накапливаются сообщения, пока они не получены клиентской программой). Поч- товый сервер — это просто машина (или виртуальная машина), обрабатывающая отправку и получение сообщений и взаимодействующая с другими машинами в про- цессе обработки сообщений электронной почты. В компании может применяться следующий общий сценарий. Некоторые служа- щие компании пользуются почтовым клиентом Microsoft Outlook, который соеди- няется с почтовым сервером компании (на нем может выполняться программа Exchange). Все сообщения электронной почты, поступающие в компанию и исходя- щие из нее, обрабатываются почтовым сервером. Для любых сообщений, отправля- емых из компании в Интернет, в одно и то же время этот сервер действует и как конечная, и как начальная точка:
310 Глава 9 Здесь представлен упрощенный случай, хотя многие компоненты имеют несколько серверов для обеспечения функционирования электронной почцгы. Обратите внимание, что почтовые клиенты (например, Outlook) обмениваются с почтовым сервером с помощью SMTP, также и почтовый сервер при обмене с дру- гими машинами в Интернете использует SMTP. Кроме того, на рисунке показаны протоколы POPS (Post Office Protocol) и IMAP (Internet Message Access Protocol). Эти протоколы позволяют почтовым клиентам лишь получать доступ к электрон- ным письмам и извлекать их. Чтобы отправить почтовое сообщение, клиенты элек- тронной почты пользуются протоколом SMTP. Протоколы электронной почты Как показывает данный сценарий, в программировании электронной йочты есть три главные темы: SMTP, POP3 и IMAP. Это вовсе не значит, что ими все огра- ничивается, но они наиболее важны для нас, как разработчиков, когда мы пишем приложения, отправляющие и получающие сообщения электронной почты. Дело в том, что названные протоколы являются стандартами отправки и получения элек- тронной почты. Надо иметь в виду, что это не единственные, а наиболее распрос- траненные и принятые методы. Другие стандарты обмена сообщениями, например Х.400, предоставляют средства, альтернативные протоколам, построенным на TCP/IP. Подробнее с протоколом Х.400 можно познакомиться на сайтах http://www.itu.int/ и http://www.alvestrand.no/x400/. Почта в Интернете в значительной степени определяется несколькими стандар- тами и рекомендациями, разработанными компаниями и отдельными лицами, за- нимающимися исследованиями и развитием интернет-технологий. Эти стандарты одобрены Почтовым консорциумом Интернета (Internet Mail Consortium, IMC, http://www.imc.org) и Проблемной группой проектирования Интернета (Internet Engineering Task Force, IETF, http//www.ietf.org). В настоящее время набор почто- вых стандартов Интернета состоит из многочисленных, связанных между собой RFC, рекомендаций и положений. Не все стандарты одобрены полностью или под- креплены официальными документами IETF, но и они считаются стабильными и широко используются промышленностью для разработки программного обеспече- ния электронной почты. Многочисленные RFC доступны на Web-сайте IMC (http//www.imc.org/rfcs.html). Это.подробные документы, в которых тщательно описывается каждый аспект обра- ботки электронной почты. При обсуждении разнообразных тем этой главы затро- нем некоторые из этих RFC. В частности, рассмотрим RFC 2821, определяющий протокол SMTP, и RFC 2822, описывающий, как Должно выглядеть сообщение элек- тронной почты. Более подробную информацию об этих RFC можно получить на Web-сайте IMC. SMTP Simple Mail Transfer Protocol, или SMTP, описан в RFC 2821, он определяет взаи- модействие между почтовыми серверами, транспортирующими электронную почту. Здесь уместно отметить, что в качестве транспортного протокола SMTP глав- ным образом использует TCP/IP (см. документ RFC 1090, посвященный SMTP на Х.25). По существу, SMTP-сеанс состоит из диалога между двумя машинами, которые стремятся передать друг другу или отправить дальше сообщение электронной поч- ты. Упрощенное представление такого взаимодействия показано на рисунке:
Протоколы электронной почты 311 Как видим, SMTP-сеанс можно кратко определить как простой обмен между дву- мя компьютерами, включающий пересылку электронной почты. В этот момент поч- товое сообщение проделало по Интернету длительный путь, пока наконец не достигло машины 2, являющейся обработчиком почты для сети i-netway.com. Если, например, не существовало пользователя с таким адресом электронной почты (ад- рес был введен неверно) или у обработчика почты не было способа доставить сооб- щение, то машина должна была ответить, что данное сообщение электронной почты доставить невозможно. Аругой взгляд на SMTP-сеанс Еще один способ взглянуть на почтовый SMTP-сеанс — установить Telnet-соеди- нение с реальным сервером электронной почты. Большинство интернет-провайде- ров запрещают этот вид доступа, поскольку он позволяет хакерам и им подобным без труда получить доступ к чужой переписке, что недопустимо! На следующем при- мере просто покажем диалог между почтовым SMTP-сервером и клиентом. Если бы какой-либо сотрудник издательства Wrox попытался отправить мне почту через Telnet, диалог мог бы выглядеть приблизительно так: open i-netway.com 25 Trying. . . Connected to i-netway.com 220 I-NETWAY.COM - Server ESMTP (PMDF V4.3-10 #2381) helo wrox.com 250 I-NETWAY.COM OK, WROX.COM. mall from:<edltor@wrox.com> 250 Address Ok. rcpt to:<krowczyk@i-netway.com> 250 krowczyk@i-netway.com OK. data 354 Enter mail, end with a single SUBJECT:E-mail Chapter Andy, thanks for the.chapter! 250 OK. // сообщение отправлено quit 221 Bye received. Goodbye. Краткое обсуждение использования клиента Microsoft Telnet см. в главе 1.
312 Глава 9 Как можно видеть, сеанс начинается с команды Telnet, открывающей соедине- ние с почтовым сервером i-netway.com. За ней следует несколько разных SMTP-ко- манд, необходимых для отправки сообщения электронной почты, и несколько ответов от SMTP-сервера. Наиболее распространенные SMTP-команды мы рассмот- рели в главе 5, где построили несложное приложение-клиент SMTP, а теперь изу- чим полный перечень команд. Сводка SMTP-команд Далее приводится краткая сводка разнообразных существующих SMTP-команд. Более детальные пояснения можно прочитать в документе RFC 2821. Команда Описание HELLO (HELD) MAIL (MAIL) RECIPIENT (RCPT) Идентифицирует SMTP-клиент SMTP-серверу. Инициирует почтовую транзакцию, чтобы доставить почтуъ один или несколько почтовых ящиков. Идентифицирует получателя, которому нужнб отправить почтовые данные. Если данные отправляются более чем к одному получателю, можно использовать несколько команд RCPT DATA (DATA) f Отмечает начало почтовых данных. Данные, следующие за командой DATA, присоединяются в конец почтового буфера. Почтовые данные могут содержать любые коды из 128 символов ASCII. Конец данных отмечается последовательностью <CRLF>.<CRLF>. SEND (SEND) Инициирует почтовую транзакцию для доставки электронной почты на один или несколько терминалом. Здесь важно заметить, что “терминалом” называется экран терминала пользователя. Поскольку в настоящее время большинство клиентов не являются клиентами терминалов, команды SEND, SGML и SAML, по существу, вышли из употребления. В эту таблицу они включены для полноты, поскольку являются частью RFC. SEND или MAIL (SGML) Инициирует почтовую транзакцию для доставки электронной почты одному или нескольким терминалам или почтовым ящикам. Почтовое сообщение доставляется на терминал каждого получателя, если получатель активен на хосте и принимает терминальные сообщения, иначе оно помещается в почтовый ящик получателя. SEND и MAIL (SAML) Инициирует почтовую транзакцию для доставки почтового сообщения одному или нескольким терминалам или почтовым ящикам. Почтовое сообщение доставляется на терминал каждого получателя, если получатель активен на хосте и принимает терминальные сообщения; оно также помещается в почтовый ящик каждого получателя. RESET (RSET) VERIFY (VRFY) Аварийно завершает текущую почтовую транзакцию Все данные будут отброшены, а все буферы очищены. Получатель должен отправить ответ ОК. Предлагает получателю подтвердить, что следующий параметр является корректным именем пользователя. Если это так. возвращаются полное имя пользователя (если оно известно) и полностью определенный почтовый ящик.
Протоколы электронной почты 313 продолжение таблицы EXPAND (EXPN) Предлагает получателю подтвердить, что следующий параметр является списком рассылки, и, если это так, вернуть имена членов этого списка. В многострочном ответе возвращаются полные имена пользователей (если они известны) и полностью определенные почтовые ящики. Эта команда аналогична команде VRFY, но используется для нескольких получателей. HELP (H^LP) Предлагает получателю передать полезную информацию отправителю. Эта команда может принимать параметр (например, имя команды) и возвращает в ответе более конкретную информацию. NOOP (NOOP) Эта команда не оказывает никакого воздействия, не считая того, что получатель должен отправить.ответ ОК. Никакая операция не выполняется, и никакие команды и данные ею не затрагиваются. QUIT (QUIT) TURN (TURN) Получив команду QUIT, получатель должен отправить ответ ОК и закрыть соединение с отправителем. Получив команду TURN, получатель должен или (1) отправить ответ ОК и принять роль SMTP-клиента, или (2) отправить отказ и сохранить роль SMTP-сервера. По соображениям безопасности эту команду рекомендовано исключить. Слабая реализация аутентификации могла бы позволить клиентской машине принять на себя роль SMTP-сервера и направлять в другую сторону сообщения электронной почты. Коды ответов Если вернуться к примеру с отправкой электронной почты через Telnet, то мож- но увидеть, что сервер отправляет коды ответов на каждую SMTP-команду. На самом деле, коды ответов сервера дают много информации о состоянии текущей транзак- ции электронной почты. Рассмотрим типичный код ответа и информацию, кото- рую он содержит: I Первая цифра означает I успех, неудачу г или незавершенность F -и Вторая цифра — 1 это категооия сообшс ▼ я Третья цифра > 354 т- задает конкретное ij сообщение в категории . ' ® это категория сообщения об ошибке , Поскольку наиболее важны первые две цифры кода ответа SMTP, посмотрим, что они означают. Обратите внимание, что первая цифра дает только статус коман- ды. Последующие цифры сообщают гораздо подробнее, чем вызван успех транзак- ции или неудача. J □ Первая цифра 1 указывает предварительный прием команды, ожидание подтверждения 2 указывает успешное завершение команды 3 указывает промежуточный прием команды, ожидание дополнительной информации 4 указывает временно негативное состояние 5 указывает неудачное завершение
314 Глава 9 □ Вторая цифра О синтаксис 1 соединение 5 почта Чтобы увидеть, как это действует, рассмотрим несколько наиболее общих кодов ответов, которые получаем после вызова транзакции SMTP. Сообщения об ошибках и состоянии могут различаться от сервера к серверу, но код ответа в каждом случае означает одно и то же. В общем, сообщение о состоянии — это лишь удобное для пользователя описание ошибочной ситуации. Код ответа Сообщение 500 Синтаксическая ошибка, команда нераспознана 501 Синтаксическая ошибка в параметрах 502 Команда не реализована 503 Неверная последовательность команд 220 <домен> Служба готова 221 <домен> Служба закрывает канал передачи 421 <домен> Служба недоступна, канал передачи закрывается 250 Удачное завершение почтового действия 354 г Начать ввод почты, закончить последовательностью <CRLF>.<CRLF> 550 Запрошенное действие не выполнено, почтовый ящик недоступен 553 Запрошенное действие не выполнено, имя почтового ящика не разрешено 554 Крах транзакции . В RFC 2821 определены все доступные коды ответов и зафиксированы все чис- ловые коды. Это означает, что новые, более подходящие для вас коды вы опре- делить не можете! Однако, поскольку речь идет о согласованном стандартном протоколе, коды ответов должны быть определены заранее и окончательно. ' Типичное сообщение электронной почты Прежде чем начнем обсуждать доступ к электронной почте через протоколы POP3 и IMAP, вкратце затронем содержание и структуру почтового сообщения, по- скольку о них идет речь в другом важном документе-стандарте, о котором говори- лось ранее. RFC 2822 описывает, как должно выглядеть сообщение электронной почты. Считайте это своего рода схемой или эскизом типичного почтового сообще- ния. Далее приведено типичное почтовое сообщение: Received: from MAILCLUSTER [111.111.111.111] by mail.brinkster.conf with ESMTP (SMTPD32-6.05) id AF1F18B600FA; Fri, 28 Jun 2002 05:40:47 -0400 Received: by MAILCLUSTER with Internet Mail Service (5.5.2653.19) id <NZPCN64X>; Fri, 28 Jun 2002 10:39:30 +0100 Message-ID: <E12F1784B51ED5119EA900DOB74D69240EFEAF87@MAILCLUSTER> From: Wrox Press <wrox@wrox.com> To: ”*Andrew Krowczyk’" <krowczyk@i-netway.com>
Протоколы электронной почты 315 Subject: C# Networking Chapter Date: Fri, 28 Jun 2002 10:39:28 +0100 Importance: high X-Priority: 1 MIME-Version: 1.0 X-Mailer: Internet Mail Service (5.5.2653.19) Content-Type: text/plain X-RCPT-TO: <krowczyk@i-netway.com> X-UlDL: 323073316 Status: U Andy, thanks for the work you’ve done! -Wrox Press । Структура типичного сообщения электронной почты состоит из заголовков со- общения и текста сообщения. Некоторая информация должна обязательно присут- ствовать в сообщении, чтобы оно соответствовало стандартам электронной почты, в то время как другая информация необязательна и может включаться или исклю- чаться в зависимости от многих факторов. Обязательная информация Несколько заголовков должны присутствовать в сообщении электронной почты обязательно: □ FROM: агент (лицо, система или процесс), создавший сообщение. Это дол- жен быть единственный удостоверенный адрес машины, сгенерированный агентом-отправителем. Как можно догадаться, эта информация сообщает, кто или что отправляет сообщение электронной почты. □ DATE: дата и время отправления сообщения. Необязательными частями этой спецификации являются только день недели и секунды. Часовой пояс может задаваться в обычных обозначениях, например CST, EDT, GMT. Считается предпочтительным указание часового пояса в виде числового смещения от GMT. В приведенном выше сообщении +0100 обозначает смещение от GMT. □ Один адрес получателя: должен присутствовать, по крайней мере, один ад- рес получателя, который может быть То, Сс или Вес. Некоторая дополнительная информация Другие заголовки и другая информация могу г дополнительно присутствовать, но необязательно для всех сообщений. Некоторые из них перечислены ниже: □ REPLY-ТО: заголовок адресата ответа часто используется, чтобы указать предпочтительный адрес электронной почты для отправки ответов. Он час- то применяется при доставке почты по списку и другими процессами, чтобы корректно задать местоположение адреса для ответа. □ SENDER: почему отправителя задавать необязательно? Потому что факти- чески отправитель задается в обязательном поле FROM (см. выше). Данное поле предполагается использовать, если отправитель электронной почты не является ее автором или принадлежит к группе авторов. Его не следует запол- нять, если оно идентично полю FROM. Поле SENDER должно присутствовать, если оно отличается от поля FROM. Например, поле SENDER можно было бы использовать следующим образом:
316 Глава 9 FROM: “Joe Someone” <joe@someone.com> SENDER: INFO-SAMPLE Discussion <INFO-SAMPLE@SOMEDOMAIN.COM> TO: Multiple recipients of list INFO-SAMPLE Discussion <INFO- SAMPLE@SOMEDOMAIN.COM> Заголовки получения К наиболее важным частям сообщения электронной почты относятся данные, передаваемые в заголовках Received почтового сообщения. Они предоставляют от- ладочную информацию и позволяют точно узнать, откуда послано сообщение и как оно добралось из точки А в точку В. В приведенном выше примере содержались следующие строки с заголовками по- лучения: Received: from MAILCLUSTER [111.111 111.111] by mail:brinkster.com with ESMTP (SMTPD32-6.05) id AF1F18B600FA; Fri, 28 Jun 2002 05:40:47 -0400 Received: by MAILCLUSTER with Internet Mail Service (5.5.2653.19) id <NZPCN64X>; Fri, 28 Jun 2002 10:39:30 +0100 Эти строки говорят о многих вещах: О Для почтмейстера они являются главным инструментом отладки. Читая заго- ловок, можно многое понять о том, откуда отправлена почта и как она попала в пункт назначения. □ Они рассказывают, какие системы затрагивали почтовое сообщение и, следо- вательно, могли его исказить. □ Каждый МТА (агент передачи почты), ретранслирующий сообщение, присо- единяет собственную строку заголовка Received. □ RFC 2882 требует, чтобы все МТА при обработке почты добавляли собствен- ные строки получения, и запрещает прикасаться к строкам получения, встав- ленным в сообщение другими почтовыми клиентами. □ Заголовки получения показывают шаг за шагом путь, пройденный почтой от отправителя до получателя. Пример прослеживания сообщений через строки получения в заголовках пока- зан на следующей схеме: Received: from host 2 by host 3 Received: by host 2 from host 1 Received: by host 1 from host 0 p p? * Received: by host 0 • ’У" •'
Протоколы электронной почты 317 Просмотр заголовков в Outlook Поскольку в заголовках содержится так много информации, стоит знать, как просматривать данные заголовков из Microsoft Outlook. Сначала щелкните правой кнопкой по сообщению, для которого вы хотите просмотреть информацию заго- ловков и выберите в меню пункт Options. Заголовки Интернета для этого сообще- ния отображаются снизу в открывающемся в результате диалоговом окне: Отметим, что заголовки Интернета могут не отображаться для почтовых сообщений, пересылаемых внутри одной сети. Как обстоит дело с MIME? . Теперь, когда получены элементарные сведения о почтовых сообщениях и про- изошло знакомство с протоколом электронной почты, возникает вопрос: “Как об- стоит дело с дополнениями?” Первоначально электронная почта была системой обмена только текстовыми сообщениями, в которых использовались только симво- лы ASCII. Но очень быстро возникла потребность присоединять к сообщениям электронной почты двоичные файлы разных типов и передавать эти дополнения вместе с сообщениями ASCII-текста. Поэтому, чтобы удовлетворить эту потреб- ность, был разработан набор стандартов многоцелевых расширений электронной почты в Интернете (Multi-purpose Internet Mail Extensions, или MIME). В MIME определяются расширения протокола SMTP, поддерживающие двоичные дополне- ния произвольного формата. Когда мы рассматриваем, из чего состоит MIME, выясняется, что фактически он выполняет две главные функции:
318 Глава 9 □ MIME кодирует двоичные данные так, чтобы их можно было передать через Интернет. Здесь следует отметить два момента: □ Не надо забывать, что Интернет — это семибитовый ASCII-мир. □ Восьмибитовые расширения не работают, поскольку возникают проблемы с длиной строки и форматированием файлов. □ MIME присоединяет к кодированным данным метку или тег, чтобы в конеч- ной точке сообщениягможно было определить и интерпретировать содержа- ние. Например, это может быть файл с фильмом или документ Microsoft Excel. V В MIME используется новая схема кодирования, которая называется BASE64. Также добавляются новые SMTP-заголовки, описывающие присоединенный доку- мент. Сама идея довольно проста — мы кодируем двоичные данные, получая разные битовые представления, и затем совмещаем эти данные с самой электронной поч- той как дополнительную информацию. Получив сообщение, клиент электронной почты из заголовков узнает, что к почтовому сообщению присоединен дополни- тельный документ (или документы), и может должным образом декодировать и ото- бразить документ. RFC, определяющие MIME и его состав, имеют номера с 2045 по 2049. Заголовки MIME ‘ Если рассмотреть заголовки, появляющиеся, когда MIME используется, чтобы к сообщению присоединить некоторый массив данных, то обнаружится, что могут использоваться несколько полей. Сначала рассмотрим обязательное поле: Обязательное поле ' f Описание MIME-Version В этом поле указывается, какая версия MIME используется (в настоящее время 1.0). Теперь перейдем к необязательным полям: Дополнительные поля Описание Content-type В этом поле описывается, какой формат имеет эта часть сообщения: текст, сообщение, приложение, многочастный формат, изображение, - аудио или другой. По умолчанию используется текст ASCII. - К наиболее распространенным типам содержания относятся* - text/plain - text/html - application/binary - application/postscript - image/gif - image/jpeg
Протоколы электронной почты 319 продолжение таблицы Content-transfer.encodin g В этом заголовке говорится, как декодировать сообщение. Могут использоваться несколько разных схем декодирования - Base64. Эта схема применяется для перекодирования двоичных данных в семибитовые ASCII-данные. I 7-bit. Перекодирование отсутствует, не учитывается регистр клавиатуры. - 8-bit. Перекодирование отсутствует. - binary. Перекодирование отсутствует. x-token — обозначение оригинального кодирования. Символ х указывает, что это схема кодирования с нестандартным статусов. Это просто означает, что содержание не кодируется по стандартизованной схеме, что в большинстве случаев является недостатком, поскольку не соответствует нормам, обеспечивающим способность к взаимодействию. Content-ID Поле идентификатора, позволяющего одной части ссылаться на другую. Аналогично полю Message-ID (уникального идентификатора в структуре сообщения электронной почты). Как правило это поле использовать необязательно. Content-description Текстовое описание кодирования данных. Например, тип содержания image может содержать “изображение луны”. Content-disposition Поле, в котором клиенту электронной почты сообщается, должно ли содержание отображаться совместно с сообщением или как дополнение к сообщению. В поле есть параметр Filename, предлагающее имя для файла, если данные отсоединяются от почтового сообщения. Теперь коротко рассмотрим типичное почтовое сообщение, включающее MIME-дополнение: From: krowczyk@i-netway.com (Andrew Krowczyk) Subject: Sample message with Word Document Attachment To: mailto:editor@wrox.com MIME-version: 1.0 Content-type: MULTIPART/MIXED; B0UNDARY=”Boundary_[ID_nf991kyavAuSo/HeKKQ] “- Boundary_[ID_nf991kyavAuSo/HeKKQ] Content-type: text/plain; charset=us-ascii Hi, please see the attached Word Document. -Andy-Boundary_[ID_nf99IkyavAuSo/HeKKQ] Date: Fri, 3 Jul 2002 16:43:34 -0700 Content-type: application/mac-binhex40; name=sample_worddoc.doc Content-disposition:, attachment; filename=sample_worddoc.doc <Word Document etc. - . . below here> PGh0bWw+DQo8aGVhZD4NCjx0aXRsZT6q967mpcC/y7hgrKGwyjwvdG10bGU+DQo8bWV0YSBodHRw LWVxdW12PSJDb250ZW50LVR5cGUiIGNvbnRlbnQ9InRleH0vaHRtbDsgY2hhcnNldDliaWcllj4N CjxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iaHR0cDovL3d3dy5raW5nLmNvbS50dy9wbS5j c3MiIHR5cGU9InRleH0vY3NzIj4NCjwvaGVhZD4NCjxzY3JpcH0gbGFuZ3VhZ2U9IkphdmFTY3Jp c H0iPg0KZnVuY3Rpb24gZ290b3VybChkaX10eXBlKSB7DQogdmFyIHB3PXdpb mRvdy5vcGVuKCdo
320 , Глава 9 Как можно видеть, секции и заголовки MIME определяют границы сообщений и двоичное содержание. Для краткости пропущен реальный двоичный код докумен- та. В символах ASCII он бы выглядел как набор искореженных данных. Я рассказал лишь о нескольких моментах, связанных с MIME-дополнениями, стремясь дать сжатый высокоуровневый обзор о включении этих дополнений в со- общения электронной почты SMTP. Ниже рассмотрим пример приложения .NET с отправкой сообщений электронной почты, включающих дополнения. Будем наде- яться, что этот обзор MIME поможет понять, какие фоновые события вызывают вы- полнение кода для платформы .NET, когда отправляются сообщения электронной почты. Теперь, когда мы рассмотрели, что происходит при отправке электронной почты, перейдем к протоколам и методам получения почтовых сообщений. Полумение электронной почты в системе клиент-сервер Рассматривая, как сообщения электронной почты извлекаются из хранилищ со- общений почтовых серверов, мы, как правило, относим эти процессы к взаимодей- ствию клиента с сервером. Для извлечения сообщений из почтового ящика центрального почтового сервера обращаемся к помощи почтового клиента: Microsoft Outlook, Lotus Notes или какого-либо еще пакета электронной почты. В Web используются и другие службы электронной почты, например Hotmail, все они следуют тем же принципам. Электронная почта главным образом обрабатыва- ется в соответствии с тремя моделями: О Автономная модель (модель POP3) □ Клиент соединяется с почтовым сервером и извлекает электронную почту. □ В этом примере вся почта хранится не на сервере, а на клиентской машине. Обычно после извлечения почта удаляется с сервера, хотя некоторые почто- вые клиенты дают возможность оставлять сообщения на сервере. □ Оперативная модель (Исходная модель IMAP) □ Клиентское приложение соединяется с сервером для каждой транзакции. □ Все хранится на сервере. d Отсоединенная модель (Более поздняя модель IMAP) □ Клиент и сервер совместно используют хранилище сообщений. □ Сервер всегда точен, а приложение-клиент должно “синхронизировать” Спи- сок сообщений с сервером. Один из самых важных моментов заключается в том, что POP и IMAP лишь полу- чают почтовые сообщения из почтового ящика сервера. Как и во всех транзакциях электронной почты, отправка почты требует использования SMTP. В следующих нескольких разделах немного подробнее обсудим получение электронной почты че- рез протоколы POP и IMAP. После этого перейдем к рассмотрению кода для .NET, выполняющего все действия, которые до сих пор обсуждались. POP3 Фактически распространение получили несколько версий протокола POP. С точки зрения протокола стандарты POP2 и POP3 вообще несовместимы. Но по- скольку огромное большинство POP-клиентов используют POP3, ограничимся об- суждением этого протокола.
Протоколы электронной почты 321 Так что же дает протокол POP3? Как известно, POP3 позволяет клиенту только извлекать почту с почтового сервера. С помощью этого протокола можно разраба- тывать привлекательные GUI-приложения, представляющие электронную почту так, что ее легко читать и ею легко управлять. За справками можно обращаться к RFC 1939, где POP3 описан более подробно. В мире POP3 почта возникает на почтовом сервере при получении системой. Почтовое сообщение находится на сервере, ожидая, когда его заберет почтовый клиент. При соединении с почтовым сервером из программы почтового клиента клиент POP3 кодирует почту с сервера на жесткий диск локальной машины. Считы- вая почту на клиентскую машину, этот метод ограничивает возможности. Какое это имеет значение? В зависимости от ситуации доступ через POP3 может быть неиде- альным. Например, предположим, мы владеем почтовой учетной записью, которую читаем на работе и дома. Предположим, на наших машинах и в офисе, и дома уста- новлена программа Microsoft Outlook. Если мы соединяемся с сервером на работе и Outlook копирует четыре новых почтовых сообщения, они обычно удаляются с сервера и передаются на жесткий диск машины в офисе. Это значит, что доступ к этим сообщениям из дома для нас потерян. Поскольку POP3 перемещает сообщения с сервера на вашу клиентскую машину, совместное использование построенной на POP3 системы электронной почты с нескольких машин может вызывать проблемы. POP3 через Telnet Раскрывая эту тему, я собираюсь быстро показать шаги и процедуры, позволяю- щие обратиться к типичному почтовому ящику POP3 на почтовом сервере. Внут- ренние методы пространства имен System,Web.Mail в .NET C# при извлечении сообщений с использованием протокола POP3 должны делать что-то очень похо- жее. Хотя значительная часть этих действий скрывается компонентами среды вы- полнения .NET, интересно узнать, что же творится за кулисами. open mail.someserver.com 110 Trying. . . Connected to MAIL.SOMESERVER.COM +0K test.someserver.COM MultiNet POP3 Server Process v4.0(l) at Fri 20-Jun-2002 3:21PM-CST user krowczyk //указываем пользователя +0K User Name (krowczyk) ok. Password, please. pass thisismypassword //вводим пароль +0K 3 messages in folder INBOX (V4.0) list 2 . //list дает размер сообщения . +0K 2 7124 //в байтах stat //stat дает общий размер +0К 3 14749 // сообщения в байтах quit +0К POP# MultiNet test.somewhere.COM Server exiting (3 INBQX messages left) Connection closed by Foreign Host
322 Глава 9 POP-команды Типичный сеанс протокола POP состоит из соединения клиента с сервером POP3, при этом используется порт 110 TCP (порт по умолчанию). Как только соеди- нение с клиентом установлено, сервер POP3 отправляет обратно сообщение, под- тверждающее соединение, и начинает сеанс POP3. Затем клиент и сервер отправляют друг другу команды и ответы до закрытия соединения и завершения сеанса. В соответствии С RFC команда POP3 состоит из ключевого слова, после которо- го могут, хотя и необязательно, следовать параметры. Эти команды имеют в том числе следующие особенности: □ Команды завершаются последовательностью CRLF. □ Ключевые слова отделяются символом пробела. □ Длина ключевого слова равна трем или четырем символам. □ Длина каждого параметра может достигать 40 символов. О Длина ответа может быть до 512 символов. □ +0К означает положительный ответ. о -ERR означает отрицательный ответ или условие ошибки. Обязательные Описание команды USER [имя] Имя пользователя, отправляемое серверу PASS [пароль] Пароль QUIT Завершает текущий сеанс DELE [сообщ] Удаляет почтовое сообщение с сервера RSET Аннулирует любые изменения, выполненные в ходе текущего сеанса STAT Возвращает число сообщений на сервере RETR [ сообщ] Извлекает содержание сообщения LIST [сообщ] Возвращает информацию о сообщении, заданном параметром, например размер в байтах. Если параметр не задан, возвращается список всех сообщений и их размеров NOOP Не выполняет никакого действия, но вынуждает сервер дать положительный ответ Необязательные команды Описание ТОР [сообщ] [и] Сервер отправляет заголовки сообщения, пустую строку, отделяющую заголовки от основной части, и затем число строк основной части указанного сообщения; [сообщ] задает номер сообщения, [п] указывает получение первых л строк.
Протоколы электронной почты ____ _ 323 продолжение таблицы UIDL [сообщ] Если параметр задан, сервер выдает положительный ответ со строкой, содержащей информацию об указанном сообщении. Эта строка называется “листингом уникального идентификатора”. Уникальный идентификатор сообщения — это произвольная определенная сервером строка, имеющая' длину от 1 до 70, состоящая из символов в диапазоне от 0x21 до 0х7Е и уникально определяющая сообщение в почтовом тайнике. APOP [mailbox] [digest] Строка, идентифицирующая почтовый ящик, и строка дайджеста MD5. Алгоритм MD5 принимает на входе сообщение произвольной длины и генерирует 128-битовый дайджест, характеризующий сообщение. Обычно используется с RSA и другими подобными системами шифрования. Протокол POP3 обеспечивает основную функциональность для получения дан- ных от почтового сервера. Он очень хорошо выполняет свое предназначение и в значительной степени является тем стандартом, которым пользуется большин- ство почтовых серверов и клиентов. IMAP Протокол IMAP дает все то, чего мы не получали от POP3. Он позволяет управ- лять папками как на клиентской машине, так и на сервере, а также обеспечивает рас- ширенную аутентификацию (в POP3 аутентификация довольно слаба) и позволяет использовать несколько почтовых серверов, которые могут работать с одним и тем же почтовым клиентом. Рассмотрим вкратце, как могут взаимодействовать клиент и сервер IMAP: Протокол IMAP4 описан и документирован в RFC 2060. Помимо прочих в этом протоколе есть следующие средства: Управление почтовым ящиком на стороне сервера □ Возможность добавлять сообщения в удаленный почтовый ящик □ Поддержка одновременных операций обновления совместно используемых почтовых ящиков и уведомление об этих обновлениях
324 Глава 9 О Уведомление о новой почте Поддержка нескольких почтовых ящиков □ Управление удаленной папкой: list/create/deiete/rename □ Поддержка иерархии папок □ Доступ к другим типам сообщений, кроме электронной почты, наприм’ер NetNews Оптимизация производительности в реальном времени □ Обеспечение определения структуры сообщения без копирования его це- ликом □ Выборочное получение MIME-порций основной части сообщения □ Поиск и получение сообщений на сервере с минимальной передачей данных Усовершенствования для перемещающихся пользователей □ Совместно используемые папки снимают ограничения POP3 по доступу более чем с одной машины □ Оптимизация производительности помогает при медленных соединениях с почтовым сервером Хотя IMAP по сравнению с POP3 дает гораздо больше дополнительных возмож- ностей, вы обнаружите, что многие пользователи и клиенты электронной почты во всем мире для получения почты исцользуют POP3. Реализовать протокол IMAP очень часто оказывается сложнее, чем POP3. В автономном режиме POP3 и IMAP имеют почти равные возможности, но для оперативного и отсоединенного режи- мов IMAP, естественно, мощнее. Однако из-за присущей протоколу POP3 простоты в оставшейся части этой главы более подробно обсудим именно реализацию POP3. .NET и электронная почта Теперь, закончив с обзором почтовых протоколов, можем перейти к самой ин- тересной информации. Если вы разобрались в способах работы протоколов SMTP, POP3 и IMAP, то понять представленный здесь код для вас не составит труда. Для начала рассмотрим, как можно отправить почту, используя пространство имен .NET System.Web. Mail, которое позволяет отправлять почтовые сообщения с дополнениями по протоколу SMTP. Также разработаем примеры приложений, ре- ализующие POP3 и NNTP (Network News Transport Protocol — протокол, используе- мый для доступа к группам новостей, очень похожий на SMTP). SMTP Когда еще программировали без .NET, для отправки электронной почты вы мог- ли использовать в приложениях технологию CDONTS. Обычно это делалось из ASP и Visual Basic. Часто вместо CDONTS применялись ActiveX-компоненты независи- мых изготовителей, несколько облегчавшие отправку почты по сравнению с пута- ными объектами Microsoft. В платформе .NET компания Microsoft собрала всю функциональность SMTP и поместила ее непосредственно в .NET Framework в про- странство имен System. Web. Mail. Используя классы из этого пространства имен, мож- но без труда создавать и отправлять почту, с дополнениями или без них, обращаясь к службе SMTP, встроенной в IIS. Использование пространства имен System.Web. Mail позволяет отправлять со- общения через компонент сообщений CDOSYS (Collaboration Data Objects для
Протоколы электронной почты 325 Windows 2000). Фактически .NET просто создает оболочку для функциональных возможностей базовых компонентов обмена сообщениями. Такой подход позволя- ет доставлять сообщения электронной почты или через почтовую службу SMTP, встроенную в Windows 2000, или через произвольный SMTP-сервер. По своему прошлому опыту я знаю, что компоненты независимых изготовите- лей часто предоставляют углубленные и управляемые функциональные возможнос- ти, которые нужны моим приложениями. Поэтому, если требуются более развитые возможности по сравнению с пространством имен System.Web.Mail, познакомьтесь со следующими компонентами: О EasyMail.NET (http://www.quicksoft.com). Здесь также предоставлены отлич- ная библиотека с функциями анализатора и возможности IMAP. □ aspNetEmail (http://www.ospnetemail.com) □ Smtp.NET (http://www.exclamationsoft.com/excbmationsoft/smtp.net/default.asp) Пространство имен System.Web.Mail Пространство имен System Web. Mail содержит три класса и три перечисления: Класс Описание MailAttachment Представляет дополнения почтового сообщения MailMessage Представляет само сообщение электронной почты SmtpMail Реализует отправку объекта MailMessage через SMTP Перечисление Описание MailEncoding Задает кодирование сообщения — Base64 или UUEncoae MailFormat Задает формат сообщения HTML или Text MailPriority Задает приоритет сообщения. High, Medium или Low Чтобы отправить сообщение с помощью этого пространства имен, надо сначала построить объект MailMessage, представляющий сообщение электронной почты. Установив свойства этого объекта и заполнив его информацией нашего сообще- ния, отправим его по назначению, используя класс SmtpMail. При инициализации объекта MailMessage наиболее вероятно потребуются следу- ющие свойства: Свойства класса MailMessage Описание . Attachments Список дополнений (объектов MailAttachment), передаваемых вместе с электронной почтой Вес Список разделённых точкой с запятой почтовых адресов, получающих копию почтового сообщения Blind Carbon Сору (Вес) Body Основная часть электронной почты BodyFormat Задает MailFormat для электронной почты. Может принимать значения MailFormat.Text или MailFormat.Html
326 Глава 9 продолжение таблицы Cc Список разделенных точкой с запятой почтовых адресов, получающих копию почтового сообщения Carbon Сору (Сс) From Адрес электронной почты отправителя Priority Задает MailPriority электронной почты Subject Тема электронной почты To Адрес электронной почты получателя UrlContentBase Получает или устанавливает НИР-заголовок Content-Base.w База для всех относительных URL, используемых в основной части на HTML UrlContentLocation Получает или устанавливает НИР-заголовок Content-Location для сообщения электронной почты Headers Свойство, доступное только на чтение, содержащее список нестандартных заголовков, передаваемых с сообщением Построение объекта MailMessage Поскольку пространства имен System. Web и System. Web. Mail недоступны для про- екта, пока не установлена ссылка на сборку System.Web.dll, начинать построение объекта MailMessage следует с установки этой ссылки: %% с.-Ж| JI domponferttName | Type | Source | 5y5tem.Web.dl C:\WINNAMfcro5oft.NEnFra. C:\WINNT\Microscift.NET\Fra. C:\WINNT\Moosoft.NET\Fra. Cl’VlNNTyCrosoft NET\Fra. Г'Шгллглт FUmW rrrxrJt Vi., C:\WINNT\FtoO5oft.NEI\Fra. C:\WlNNT\Microsoft.NET\Fra. C:\WINNT\MKtO5oft.NET\Fra. C:\WINNT\MKrosoft.NET\Fra. C:\WINNTVtoosoft.NETV!ra. C:\WINN“Micr"«'frNE'nFra. 1.0.3300.0 1.0.3300.0 1.0.3300.0 1.0.3300.0 7.п,зжо System. web.Re£ularExpre*si... System. Web.Setvices.dl System.Windows.Forms.dl System.Xrnl.dl MflPrni................ 1.0.3300.0 1.0.3300.0 1.0.3300.0 1.0.3300.0 1.0.3300.0 1.0.33000 Componert 'J*ti s System.Manegement Sy$tem.Messaging.dl System.!^ n“*M.Rem0ting System.Rurilime.SenaleaUon.... System.Securty f /stem ServiceProcess c i , Ven on 1Pr _ _ _ ,№T |сом'|рдо| Add Reference i.jveb.dll i u.jXj C;’\/INtjT\M'CrO5ort.NETkFra После того как ссылка на System.Web.dll добавлена в проект, надо включить ее в оператор using и создать экземпляр объекта MailMessage: using System web.Mail£ Л // Создаем объект почтового сообщения а, t MailMessage email =». new MailMessage(.)'^^^^®^^^^Cfefc:«^
Протоколы электронной почты 327 Далее установим несколько свойств сообщения электронной почты: ^//Устанавливаем параметры сообщения ® :< £ О email. From. = "someone@someone.com”'; МЙГ U ж Ш. email-. То. = "krowczyk@i-netway.com"; ' / email.Subject = "Test message using SmtpMaiT”; email.BodyFormat - MailFormat.Text; email.Body = “This is just a test message"; . 1 Подключение дополнения Для подключения к сообщению дополнений нужно всего лишь создать один или несколько объектов MailAttachment и включить их в список Attachments объекта MailMessage. Вот основные члены класса MailAttachment: Свойства класса MailAttachment Описание Encoding Тип кодировки дополнения электронной почты. Он может принимать значения MailEncoding Base64 или MailEncoding.UUEncode Filename Имя файла дополнения. Теперь присоединим дополнение. Для этого вызываем метод Add() коллекции Attachments, передавая ему путь файла, который предполагаем присоединить к поч- те, и тип кодировки, который хотим использовать: email.Attachments.Add(new MailAttachment(@'’c:\testfile.txt", '.’X - ... MailEncoding. Base64)>; Таким способом, добавляя объекты MailAttachment к списку Attachments объекта MailMessage, можно присоединить к сообщению электронной почты любое число до- полнений. Объект SmtpMail Чтобы отправить подготовленное сообщение электронной почты, нужно вы- звать метод объекта SmtpMail. Интересно отметить, что в классе SmtpMail есть лишь один действительно важный метод—статический объект Send (). Этот метод отсыла- ет почтовое сообщение SMTP-серверу. Он может принять как параметр объект MailMessage или четыре строки, задающие отправителя, получателя, тему и основ- ную часть электронной почты. В классе SmtpMail есть также одно открытое свойство: Свойство класса SmtpMail Описание SmtpServer Это сервер, которому должна отправляться SMTP-почта. Если это свойство не установлено, по умолчанию объект SmtpMail настроен 4 на отправку почты к службе SMTP Windows. Учтите, что если SMTP-сервер не задан, то перед запуском этого кода нужно установить на вашей машине службу SMTP. Для этого выберите Control Panel I Add Remove Programs I Add Remove Windows Components I Internet Information Services I SMTP Service.
328 Глава 9 // Отправляем e-mail, используя объект SmtpMail SmtpMail.Send(emall);•—' ’ Д. f л% ._ // или в таком виде - без почтового SMTP-объекта string from = “from@somewhere.com’*; string to = *’to@somewhere.com”; string subject = ’’test”; , string body = “test message”; SmtpMail.Send (from, to, subject, body); ;«?9: 5-У SmtpMail.Send(email); gsb i . •& Если не установить свойство Sm с pSe rve г, электронная почта будет отправлена че- рез службу SMTP Windows. Для установки свойства Smt pSe rve г надо включить в при- ложение следующий код: // Например, перед отправкой сообщения мы хотим // установить поле сервера SMTP SmxpMail.SmtpServer - "mail.testserver.com’’; Ц Теперь отправляем сообщений как раньше SmtpMail.Send(email); В предыдущих фрагментах кода показаны различные части программы отправ- ки электронной почты, использующей возможности SMTP в .NET. Теперь рассмот- рим пример приложения, в котором все эти фрагменты соединяются вместе. Приложение почтового SMTP-клиента Законченный пример почтового SMTP-приложения будет простым приложени- ем Windows-форм .NET, которое позволит устанавливать разнообразные свойства электронной почты и отправлять ее, используя назначенный SMTP-сервер. На сле- дующее снимке экрана изображен конечный результат, полученный в результате выполнения кода, который мы собираемся рассмотреть. В этой форме есть пять од- нострочных текстовых полей, в которых пользователь может ввести имя SMTP-сер- вера, почтовые адреса отправителя и получателя, тему почтового сообщения и путь к любому дополнению, которое может быть отправлено вместе с почтой. В форме также есть многострочное текстовое поле, в котором можно ввести основную часть почтового сообщения:
Протоколы электронной почты 329 Этот код можно скопировать с нашего сайта из'материалов к этой главе. На са- мом деле никакого волшебства при написании такого простого приложения не тре- буется. По существу мы просто создаем форму с подходящими текстовыми полями и вводим в обработчик события щелчка по кнопке Send код, в котором пользуемся уже рассмотренными средствами SMTP для отправки почты. Важный код приложения Как и раньше, нужно добавить в проект ссылку на сборку System. Web. dll и дирек- тиву using, указывающую на пространство имен System. Web. Mail. Кроме обычного кода Windows (сгенерированного Visual Studio .NET), необходимо лишь добавить код, поддерживающий кнопку Send. Он очень похож на код, который уже рассмат- ривался: private void button1_Click(object sender, System.EventArgs e) { 1 i. j * ¥ " » // Создаем почтовое сообщение t MailMessage email » new MailMessage() ; // Устанавливаем параметры сообщения S email. From = txtFrom Text; email.To txtRecipientlText; email.Subject = txtSubject.Text; email. BodyFormat = System.Web.Mail.MailFormat.Text; email. Body = txtMessage.Text; // Подключение дополнения email.Attachments Add(new MailAttachment (txtAttachment.Text, " MailEncoding.Base64)) ; // Устанавливаем SMTP-сервер SmtpMail.SmtpServer = txtSmtpServer.Text; // Теперь отправляем сообщение SmtpMail.Send(email); } Сверьте полный код приложения с материалами нашего сайта. POP3 Если вы уже обладаете опытом создания почтовых программ в .NET, то вам, вероятно, известно, что .NET не предоставляет собственных средств для извлечения почты из почтовых ящиков через протоколы POP3 и IMAP. Мы уже рассмотрели SMTP-компонент .NET Framework, который охотно позволяет отправлять электрон- ную почту SMTP, но еще не успели сказать, что пространство имен System. Web Mai 1, по существу, является лишь оболочкой компонента CDOSYS. Этот неуправляемый ком- понент фактически используется на нижнем уровне для отправки почтовых со- общений. Так как же нам получить почту в .NET? Можно попытаться использовать CDO, но тогда мы сможем работать только с серверами Microsoft Exchange, однако не все почтовые серверы в мире базируются на Microsoft Exchange! Поэтому, принимая во внимание, что книга посвящена сетевому программированию, надо попытаться на- писать код для нашего собственного класса, который будет использовать протокол POPS для получения сообщений электронной почты. Чтобы достичь этой цели, бу- дем пользоваться некоторыми пространствами имен, существующими в .NET.
330 Глава 9 Создание РОРЗ-класса на C# При создании класса, который обрабатывает РОРЗ-сообщения, первая цель со- стоит в точном определении частей сообщения. Сначала создадим новый С#-проект Windows Application с именем POPMail. Начнем с создания класса, представляющего ядро РОРЗ-сообщения электрон- ной почты, и для этого добавим в проект новый класс P0P3EmailMessage: public class P0P3FmailMessaoe ' • { . ! ; s // Определяем открытые члены public long msg^umber; i public long ^sySize^ public bool msgReceived: 5 public string irgContent; } O • « 3 •> Л • Этот класс содержит номер сообщения, размер сообщения (в байтах), флаг, ука- зывающий, было ли сообщение получено с сервера, и строку с содержанием сообще- ния электронной почты. Если вспомнить обсуждение протокола, все это общие поля для типичного сообщения электронной почты, используемые при получении сообщения. Например, чтобы получить информацию о сообщении или получить его с РОРЗ-сервера, нужно знать индекс (msgNumber), определяющий сообщение, с которым мы хотим работать. System.Net.Sockets. TcpClient Как видно из последних нескольких глав, среда .NET Framework представляет обширный набор классов, удовлетворяющих потребности низкоуровневого про- граммирования. В данном случае мыхотим приспособить класс TcpClient, поскольку будем устанавливать удаленные соединения с почтовым сервером, используя прото- кол TCP. Таким образом, мы создадим класс, производный от пространства имен System.Net.Sockets.TcpClient: 7/ Определяем класс POP3 public class POP3 • System Net.Sockets.TcpClient { Соединение с сервером Этот класс должен уметь делать несколько вещей и, в первую очередь, устанавли- вать соединение с сервером POP3, передавая ему имя пользователя и пароль. Вспомните Telnet-сеанс из обсуждения протокола POP3, чтобы провести аналогии с тем, что будем делать сейчас. Итак, начнем с написания метода ConnectPOP(). Он принимает три параметра — имя сервера, с которым мы соединяемся, имя пользователя и пароль для почтового ящика, к которому хотим получить доступ. Заметьте, что этот метод вызывает неко- торые другие методы, представленные далее в этой главе: public void ConnectPOPTstring sserverNamb, string &UseiNamt string sPasswoid) И Сообщение и результирующий ответ от.оерйера string sMessage; ю > мхчФ-'- ‘ч string sfiesult: U Вызываем метод Connect, класса TcpClient г . - ------ - - - Г- -гу if не забываем, что по умолчанию; порт для сервера- 110 *
Протоколы электронной почты , _ 331 Connect(sServerName, 110); // Получаем результат sResult ® ResponseO; ; «V • 7,' ' ' i- ' ;Л ' . // Проверяем ответ, убеждаемся, что все +0К if (sResult.Substring(O,3) !=<*+0Kn) throw new POPException(sResult); // Соединились, отправляем имя пользователя sMessage = "USER " + sUserName + *‘\r\n”; // Метод Writef) отправляет данные через Tcp-соединение Write(sMessage); sResult ~ ResponseO; // Проверяем ответ if (sResult. Substrings. 3) ! = '‘+0K”) throw new POPException(sResult); } / // Наконец в той же манере отправляем пароль sMessage = “PASS ” + sPaSsword + ”\r\n”; Write(sMessage); sResult - ResponseO; if (sResult.Substrings,3) “+0K”) throw new POPException(sResult); Чтобы облегчить чтение кода, я снабдил его комментариями. Мы соединяемся с сервером POP3 и отправляем имя пользователя и пароль, используя метод Con- nect () класса TcpClient и наши методы Write() и Response(). Как было ранее показано в РОРЗ-сеансе Telnet, если мы соединились успешно, РОРЗ-сервер должен отпра- вить ответ +0К, а если произошла ошибка—сообщение - ERR. В случае ошибки мы по- рождаем исключение, привлекая сообщение, полученное от сервера. Если вы соединяетесь, то должны уметь отсоединяться Поскольку речь идет о соединении, теперь можно показать метод разрыва сое- динения. Чтобы отсоединиться от сервера, надо всего лишь выдать команду QUIT. Поэтому метод DisconnectP0P() получается довольно простым: public void DisconnectPOPO { ‘ i string sMessage; string sResult;/ sMessage - "QUIT\r\n”; Write(sMessage); sResult = ResponseO; if (sResult.Substrings,3) !== "+0K”) throw new POPException(sResult); / } - К этому моменту мы получили способ установления соединения с POP3- серве- ром, а также способ отсоединения от него. Типичный РОРЗ-сеанс будет состоять из вызова метода ConnectРОР(), команд получения электронной почты и вызова Dis- connectPOPO.
332 Глава 9 Получение списка сообщений Получить список сообщений в ящике POP3 можно, просто выдав серверу коман- ду LIST. В методе ListMessages() показано, как это можно сделать. public ArrayList Li st Messages () { // Приблизительно то же, что ConnectPOP и DisconnectPOP string sMessage; I string sResult; * ArrayList returnValue = new ArrayListO; sMessage = “LIST\r\n”; Write(sMessage); sResult = Response?); , if (SResult.Substring^, 3) й *+6K”) th row new POPExceation? sResult); while (true);;- i * .. . V sResult = Response?1; if (sResult ~ return jr^turnValue; Г:" } .................... else y I *. L. ; J0P3EmailMessage oMailMessage - new P0P3EmailMessage?); // Определяем разделитель г Scheru: sep ‘ ' f // Используем Meiод*Split, чтобы разбить массив данных stringtlvalues = sResult.Spllt(sep); // Понещаег данные в объект oMailMessage oMai (Message msgNumber ~ Int32,'Parse(valuesiO]); . oMa11 Message.msgSize Int32 Parse(values[1]); oMailMessage.msgHeceived = false; .'etdrnValue.Add(oMailMessage) ; continue; Как видно из примера с Telnet, отправка команды LIST РОРЗ-серверу вызовет пе- редачу сервером нескольких строк текста. Каждая строка представляет сообщение электронной почты, которое содержит номер сообщения и число байтов в этом со- общении. Важно отметить, что этот метод возвращает массив объектов сообщений, содержащих минимум данных. Способ получения более полной информации и со- общения состоит в создании отображения команды RETR, извлекающей с сервера ре- альное сообщение.
Протоколы электронной почты ~ 333 Получение конкретного сообщения Чтобы извлечь с РОРЗ-сервера полное сообщение, нужно выдать команду RETR с msgNumbe г сообщения, которое хотим получить. В этом методе следуем той же про- цедуре, которая была описана ранее: , public^ R0P3BnailMessage RfetrieveMessage(p0P3EmailMessage тьдВЕТР) string sMessage; -- string sResult; //Создаем новый экземпляр объекта й устанавливаем новые значения P0P3EmailMessage oMailMessage = new P0P3Emai IKessageO; oMailMessage.msgSize == msgRETR /nsgSize; oMailMessage msgNdtiioer = msgRETR msgK'dmber; // Для полу <ения соответствующего сообщение посылаем команду RETR sMessage “RFTh ” + msgRETR.msgNumbeг + *‘\r\n”; Write(sMessage); sResult = ResponseO, if (sResult.Substrxng(03) ! = •+0K’*) threw new P0pException (sResult); // Поскольку сообщение получено, устанавливаем во флаге значение true oMailMessage.msgReceive i '"= true; // Чтобы получить bee, выполняем цикл/ пока не встретится конец (". ”) while (true) : {. , ‘i- -5 « г- I ' sResult = ResponseO-; if (sResult == *‘.\r\n”) break; else oMailMessage.msgContent » sResult } return oMailMessage; } Удаление сообщения Поскольку согласно протоколу POP3 после получения сообщения оно остается на сервере, то чтобы его удалить с сервера, нужно вызвать команду DELE. Написать этот метод также нетрудно: public void DeleteMessage(P0P3EmailMessage msgDELE) { ' ' string sMessage; p string sResult; sMessage * “DELE ” + nsgDELE msgNumber + “\r\n”; Write(sMessage); E, sResult = ResponseO; if (sResult.Substrings, 3) ! = '*+0K") throw new POPException(sResult); } Метод WriteO Метод записи на входе принимает сообщение и записывает его в сетевой поток TCP. На самом деле он отправляет нашу команду на РОРЗ-сервер, с которым уста-
334 Глава? новлено соединение. Поскольку строковые данные C# нельзя напрямую передать в буфер сетевого потока, надо с помощью класса ASCIIEncoding пространства имен System. Text получить байтовое представление строки данных. Выполнив это преоб- разование, записываем результат в сетевой поток: private void Write(string sMessage) { a // Используется для перекодирования данных * System.Text.ASCIIEncoding oEncodedData = new System. Text. ASCIIEncodingQ;. //.Теперь пересылаем сообщение в буфер для отправки в сетевой поток TCP bytef] WriteBuffer = new byte£l024]; - WriteBuffer * oEncodedData GetBytes(sMessage) ; // Выводим содержимое буфера e поток TCP Networkstream NetStream = GetStream(); . NetStream.Write(WriteBuffer, 0, WriteBuffer.Length); j . • , /. Метод ResponseO Если перевернуть метод Write() наоборот, то получится метод ResponseO. Он по- зволяет считать данные по соединению с РОРЗ-сервером. Этот метод используется во многих методах класса для получения кодов результата, отправляемых сервером в ответ на команды, отсылаемые ему через вызовы метода Write (). Здесь тоже нужно обратиться к классу ASCIIEncoding, чтобы получить строковое представление бай- тов, полученных из сетевого потока. Код этого метода должен быть понятен без дополнительных пояснений: private String ResponseO { ' System.Text.ASCIIEncoding oEncodedData = new System.Text.ASCIIEncodingO; byte [] ServerBuffer = new Byte[1024]; Networkstream NetStream = GetStream(); " int count * 0; // Здесь считываем данные из сетевого потока сервера и помещаем, их // в буфер (чтобы позднее декодировать) while (true) г- { , V Mr 2 —й byte [] buff = new Byte[2j. - f int bytes = NetStream.Read( buff, 0, 1 ).;/ if (bytes == 1) ServerBuffertcount] » buff£O]; ;, count++; if (buff£01 .= r\n‘) break;, i else . break; I } " * // Возвращаем декодированное строковое ASCII-значение -
Протоколы электроннойпочты 335 string ReturnValui.' * pEncodedData.GetStringCServerBuffer, 0, count); return ReturnValue; - - * . » v . t ,? .. -* Класс PopException Этот класс просто инкапсулирует исключение приложения, которое можно по- рождать в нашем коде: namespace POPMailException { * ' ; ’ - у. public class POPException : System.ApplicationException { - • • . ! public POPException(string str) : base(str) { } Упрощенный пример консольного приложения Полный код представленного здесь примера приложения можно найти в мате- риалах к этой книге на сайте www.wrox.com. В примере к только что написанному классу POP3 добавлен внешний GUI-интерфейс, но простейшее применение этого класса можно увидеть с помощью консольного приложения, метод Main() которого приводится далее. В этом методе выполняются действия, необходимые для созда- ния экземпляра класса, получения номеров сообщений и текста сообщения: static void Maln(string[] args) { • t try { < t .: .: . .• . •• POP3 oPOP = new P0P3Q;; oPOP.ConnectPOP("mail,someserver,com”, "username”, "password”); ArrayList Messagelist = oPOP.UstMessages(); fbreach (P0P3EmailMessage POPMsg in MessageList) ( P0P3EmaiIMessage POPMsgContent — oPOP.RetrieveMessage(POPMsg); System.Console.WriteLine("Message {0}: {1}’’, POPMsgContent.msgNumbe r, POPMsgConteht,msgCdntent); } oPOP.DisconnectPOP(); catch ( POPException e ) { System.Console.WriteLine(e-ToSt ring()) ; catch ( System. Exception e) { System.Console.WriteLine(e.ToString()); На следующем рисунке представлен снимок экрана написанного мной учебного приложения PopMail. Оно использует рассмотренный класс POP3 для получения списка сообщений электронной почты, хранящихся в моем почтовом ящике. Поль- зователь может получить текст, введя номер сообщения, которое нужно отобра- зить. Тогда из почтового ящика получается все сообщение:
336 Глава 9 Расширение этого класса Этот класс можно усовершенствовать и расширить многими способами. Может потребоваться расширить класс P0P3EmailMessage дополнительными возможностя- ми, налример, включить отдельные свойства, описывающие заголовки, тему, сооб- щение и т. д. Разрабатывая представленный вам класс, я хотел сделать его простым в использовании и старался придерживаться того типа конструкции, который мож- но наблюдать в Telnet-сеансе с РОРЗ-сервером. В таком виде они больше соответ- ствуют духу протокола POP3 и могут быть полезны для понимания принципов сетевого программирования. Многие популярные компоненты независимых изготовителей безупречно реа- лизуют эти функциональные возможности. Но какая нам радость в том, чтобы про- сто купить один из них? NNTP Протокол NNTP широко используется для доступа к содержанию групп ново- стей в Интернете. Этот протокол определен в RFC 977 и пользуется популярностью довольно давно. Кратко представим класс, инкапсулирующий доступ к группам но- востей и серверам NNTP, который базируется на описанном ранее классе POP3 и очень на него похож. Чем похож? Во-первых, это простой, базирующийся на сете- вых потоках TCP-протокол, как SMTP и другие, связанные с электронной почтой протоколы. До этого мы не рассматривали NNTP подробно, поскольку данная глава ориентирована на рассмотрение платформы .NET и доступа к электронной почте. Поэтому, если возникнет желание узнать больше о протоколе NNTP, про- читайте документ RFC 977, к которому можно получить доступ через http://WWW ietf.org/rfc/rfc0977.txt?number=977. Чтобы было легче разбирать- ся в коде, я приведу перечень распространенных команд и ответов, которые могут
Протоколы электронной почты 337 участвовать при обмене с NNTP-сервером. Они очень напоминают элементы прото- кола POP3, перечисленные в предыдущих примерах. NNTP-команды Наиболее распространенные команды, выдаваемые NNTP-серверу групп ново- стей, перечислены в следующей таблице: NNTP-команды Описание ARTICLE Отображает заголовок, пустую строку и текст основной части текущей или заданной статьи GROUP Возвращает номера первой и последней статей в группе и оценивает число статей в файле группы. LAST Внутренне поддерживаемый “указатель текущей статьи” устанавливается на предыдущую статью текущей группы новостей. LIST Возвращает список действующих групп новостей и сопутствующую информацию. NEWSGROUPS Список групп новостей, созданных начиная с указанных даты и времени, будет передан в таком же формате, как ответ на команду LIST. NEWNEWS Список идентификаторов сообщений для статей, полученных в заданной группе новостей, начиная с указанной даты. NEXT Внутренне поддерживаемый "указатель на текущую статью” продвигается на следующую статью в текущей группе новостей. Если в текущей группе больше нет статей, возвращается сообщение об ошибке и статья остается выбранной. POST Если передача на сервер разрешена, статья передается на сервер. QUIT Закрывает соединение с сервером. NNTP-ответы Далее перечислены наиболее часто используемые ответы, получаемые от NNTP-сервера групп новостей. Приводятся не конкретные ответы, а перечень зна- чений цифр, составляющих NNTP-ответ. Более полный список ответов можно най- ти в RFC. NNTP-ответы Описания 1хх Информационное сообщение 2 хх Команда выполнена Зхх Часть команды выполнена, отправляете остаток 4хх Команда корректна, но по каким-либо причинам не может быть выполнена 5хх Команда не реализована, некорректна или серьезная программная ошибка хОх Соединение, установка и разные сообщения
338 Глава 9 продолжение таблицы x1x Выбор группы новостей x2x Выбор статьи x3x Функции распределения x4x Отсылка данных на сервер x8x Нестандартные расширения (частные реализации) x9x Отладка вывбда К наиболее общим относятся следующие коды ответов: Общие сообщения Описание 100 Справочный текст 190-199 Отладочный вывод 200 Сервер готов — отсылка данных разрешена 201 Сервер готов — отсылка данных не разрешена 400 Сервис остановлен 500 Команда не опознана 501 Ошибка в синтаксисе команды 502 Ограничение доступа или в разрешении отказано 503 Ошибка программы — команда не выполнена С более конкретными ответами и командами познакомимся, когда будем обсуж- дать код класса NNTP. Создание класса NNTP в C# Объясняя этот класс, будем придерживаться той же схемы представления, кото- рую применяли для класса POP3. Класс NNTP произведем от того же класса System Net Sockets TcpClient class. Сначала импортируем необходимые простран- ства имен: using System: using System.Net.SocKets; using NNTPServerException; using System.Collections; // наша реализация класса исключения Наследование классу ТСР-клиента Как и в РОРЗ-клиенте, наследование классу TcpClient дает обширные функ- циональные возможности, реализацией которых не нужно заниматься самим. Мы получаем уровни сетевой передачи, обеспечивающие соединение с сервером и поз- воляющие отправлять данные через сетевой поток. ^public^fass NNTP System.Net.Sockets.TcpClient^^^^e»^»!»^
Протоколы электронной почты 339 Соединение с сервером Метод ConnectNNTP() просто вызывает метод ConnectO класса TcpCIient, пере- давая ему имя сервера, а также стандартный номер порта (119) для соединения с NNTP-сервером: public void ConnectNNTP(string sServer) string sResult/' // Соединяемся с сервером на порте#119 Connect(sServer, 119); * _ sResult = ResponseQ;- // В данном случае код ответа 200 - это ответ ОК if (sResultSubstring(0. 3) “200") throw new NNTPException(sResult); Разрыв соединения с сервером Раз уж мы написали функцию соединения, то должны написать и функцию, раз- рывающую соединение. Этот метод отправляет NNTP-серверу команду QUIT: public void DisconnectNNTP() string sMessage; A string sResult; // Отправляем команду QUIT sMessage = ‘•QUIT\r\n"* Write(sMessage); sResult = ResponseQ; //Мы рассчитываем получить код 205, подтверждающий наш выход л if (sResult.Substring( 0, 3) ! = “205") throw new NNTPException(sResult) .; } Получение групп новостей Как можно догадаться, вызов метода GetNewsGroupListingO нашего класса будет использовать команду LIST, возвращающую все имеющиеся группы на NNTP-серве- ре. Их может быть масса, смотря какой сервер! public ArrayList GetNewsGroupListingO string sMessage; string sResult; // Создаем массив для возвращаемых значений. ArrayList ReturnValue = new ArrayListO; sMessage = “LIST\r\n”; Write(sMessage); // Проверяем ответ, если OK, продолжаем sResult - ResponseQ; ™ • if (sResult.Substring(O, 3) != "215") throw new NNTPException(sResult);V ... . .-ж- ......
340 _ Глава^ // Пока мы не получили все результаты, в цикле наращиваем список массивов while (true) { sResult - ResponseO; if (sResult ==? ".\r\a” || sResult == “,\n”) { return ReturnValue; else { . . chard separator = { ‘ K J; stringf] values ='sResult.Split(separator); ReturnValue.Add(values[D)); continue; } } } Получение новостей из группы Чтобы получить новости из указанной группы новостей, присутствующей на NNTP-сервере, нужно создать метод, который вызывает команду GROUP и передает ей имя группы новостей, для которой хотим получать сообщения: public Arraylist GetNews(string sNewsGronp) { string sMessage; string sResult; ArrayList ReturnValue = new ArrayListO; sMessage = “GROUP " + sNewsGroup + •,\r\n"; // Записываем сообщение на сервер Write(sMessage); sResult = Response (), // Проверяем, как завершилась операция if (sResult, Substring^, 3) J= “211”) throw new NNTPException(sResult); chard separator. = { * ’ }; stringd values = sResult.Split(separator!; // Для качала и конца long begin ь Int32.Parse(values[2j); long end = Int32 Parse(values[31);. if (begin + WO < end && end > 100) begin = end •- 100; for (long i - begin; Kend; i++) 1 sMessage ~ "ARTICLE w + 1 + “\r\nM; Write(sMessage); sResult = ResponseO; if (sResult Substring^ 3) == “423”) continue; if (sResult.Substring(0, 3) != “220’) throw new NNTPException(sResult); ,? ?. - v
Протоколыэлектронной почты 341 string sArticle = "" J while (true) sResult = ResponseO; i if (sResult == “.\r\n”) break; if (sResult == “.\n") break; ' if (sArticle.Length < 1024) sArticle += sResult; 44 } , ReturnValue.Add(sArticle); } return ReturnValue; } Отсылка данных группе Отсылка данных в группу новостей—также работа несложная. Нужно просто пе- редать команду POST с именем группы новостей и вслед за ней отправить заголовки и основную часть сообщения, которое хотим передать группе: public void PostM’essage(string sNewsGroup, string sSubject, string sFrom, v - string sContent) { ... - • string sMessage; string sResult; ( sMessage = “POST + sNewsGroup + “\r\n”; Write(sMessage); sResult = ResponseO; 1 if (sResult.Substrings, 3) 1= “340") throw new NNTPException(sResult) ; // Строим сообщение sMessage » “From: * + sFrom + “\r\n” + “Newsgroups: ” + sNewsGroup + “\r\ny + “Subject: ” + sSubject + “\r\n\r\n” + sContent + "\r\n.\r\n”; t Write(sMessage); sResult = ResponseO; if (sResult.Substrings, 3) ’= “240") throw new NNTPException(sResult); ) Метод WriteO Поскольку для транспортировки через сетевой поток требуется выполнять пе- рекодирование строковых данных C# в массив байтов, мы должны снова это пред- усмотреть в нашем методе, который записывает данные на сервер: private void Write(string sMessage) { лу' > ‘ System.Text.ASCIIEncoding oEnpode a new System.Text.ASCIIEncodingO; byte[] WriteBuffer = new byte£lC24j; - y J ^WriteBuffer-» oEncode. Get Bytes (^Message). ... .
342 Глава 9 NetworkStream oNetworkStream = GetSt reamO oNetworkStream.Write(WriteBuffer, 0, WriteBuffer.Length); Метод ResponseO Кроме того, надо преобразовать данные, возвращаемые от сервера, в соответ- ствующий строковый формат для представления в нашем классе: private string ResponseO { . System.Text.ASCIIEncoding oEncode = new System. Text. ASCIIEncodingO; byte [JServerBuffer = new byte[1024J; . Networkstream oNetworkStream = GetStream(); int count = 0; while (true) { byte [] LocalBuffer = new Byte[2J; int oytes = oNetworkStream Read(LocalBuffer, 0, 1); if (bytes == 1) { ServerBufferfcount] = LocalBuffer[0]; count++; if (LocalBufferfOJ -= ‘\n’) break; } \ else break; string ReturnValue = oEncode GetString(ServerBuffer, 0; count!; return Returnvalue; Класс NNTPException Снова определяем собственный класс исключения для ошибок NNTP: J namespace NNTPServerExceptibn I { ' ' ; public class NNTPException : System.ApplicationException public NNTPException(string str) base(str) { * ’ } } 1 » } Примерное использование класса Воспользоваться созданными классами довольно просто, как можно видеть из следующего кода (мы снова приводим метод Main() для простого консольного при- ложения). Он проводит вас через вызовы разных методов этого класса: static void Main(string[J args) try // Создаем объект NNTP NNTP oNNTP - new NNTP(); ; r
Протоколы электронной почты 343 // Соединяемся с сервером ,Л ч oNNTP.ConnectNNTP(“news.testserver.com”); Ц Получаем список групп новостей для. сервера z « ArrayList NewsGroupList = oNNTP. GetNewsGroupListingO; foreacb (string NewsGroupEntry in NewsGroupList) System.Console.WriteLine(“Newsgroup :{0}”, NewsGroupEntry); // Теперь получим новости для статьи, названной “this article” NewsGroupList = oNNTE.GetNews(“msnews.microsoft.com”); foreach (string sArticle in NewsGroupList) System.Console,'WriteLineC"{0}”, sArticle); *-' •• 'J*- • . „й# '• 1 J. ' * oNNTP.PostMessage(“test”, “test”, “test@test,com (Test User}”, "test”); , ” oNNTP. DisconnectNNTPO; ? ' catch (NNTPException e) r1' , ' (. •' " > •’ System. Console. WnteLine(e, ToSt ring()); ,i’f } Л' * i,. J '• catch (System. Exception) l 1 . ' . . 4 ASystem. Console.WriteLine(“Unhandled Exception”)i -A " V ) ? 4 ‘ !- >•- - f Jr \ У 4„лЛ:^ ’ Я также включил пример NNTP-приложения на наш сайт в копируемые материа- лы для главы. Это приложение работает очень похоже на пример POPMail, который был рассмотрен ранее. Приложение NNTPSample имеет ту же структуру, что и при- мер POPMail, но использует класс NNTP для получения корреспонденции из группы новостей: lojxl rra‘cro«/t. public accessconverjion NNTP Sample fniciosoH niblic access con mandbaisu .developers.too&utode NNTP Server. Imsnews. iricrosolt com ............. miciosoft.public.accessaclivexcontiol mictosolt.,’ /.,c. access, ark $. I г mictosoflpublic.access.dalaaccess.pages mcrosollpubEc.acce$s.developers.tootdto I’m workng m a Web App that might be used lot se vetal users, with dilletent permissions I'm using web config Hes in otdet to gtant/deny the access to those pages I want to protect and it woiks iust fine so far. Now what I want to do is tender the menu as a user control or such, but I need to know what pages the current user is alowed to
344 •. Глава 9 Расширение класса NNTP Какой код ни возьмите, его всегда можно усовершенствовать. Для класса NNTP можно выполнить те же усовершенствования, что и для класса POP3. Для начала лучше аредставить в объекте NNTP-сообщение, затем добавить фоновые реализа- ции получения сообщений и навигации. Эти изменения могли бы избавить прило- жения-клиенты, которые потребуется написать, от явных обращений к уровню протокола при работе с группами новостей. Итоги На этом мы завершаем обсуждение протоколов электронной почты. Понятно, что существует еще очень много тем, достойных изучения и активного освоения, и мы лишь поверхностно коснулись RFC и самих протоколов, оставив без ответа мас- су вопросов. Подводя итоги, напомним, что в этой главе мы в основном познакоми- лись с протоколами SMTP, POP3, IMAP и NNTP, узнали, как эти протоколы работают совместно при отправке и получении сообщений электронной почты че- рез Интернет. Мы также представили код примеров приложений, работающих с элект} }нной почтой через встроенные классы .NET Framework, которые обеспечи- вают отправку сообщений по протоколу SMTP, а также позволяют создать базовые классы реализации протоколов POP3 и SMTP. Хотелось бы остановиться подробнее на таких темах, как протоколы IMAP и MIME, но это такие серьезные и сложные протоколы, что в рамках главы раскрыть их довольно сложно. Тем не менее имеются инструменты, созданные независимы- ми изготовителями, позволяющие пользоваться этими протоколами в .NET. К со- жалению, легкого способа реализовать эти функциональные возможности самостоятельно не существует, поэтому для промышленного применения один эле- мент управления, разработанный независимым разработчиком, вероятно, обойдет- ся вам дешевле собственной реализации. Эти протоколы не так просты, как POP3 и NNTP.
ГЛАВА 10 Криптография в .NET |/ IХаждый год использование компьютеров в преступных целях впечатляюще растет. Пространство имен System.Security.Cryptography среды .NET Framework предоставляет программный доступ к разнообразным криптографическим служ- бам, которые можно включить в приложения для шифрования и дешифрования данных, обеспечения целостности данных и обработки цифровых подписей и сер- тификатов. В этой главе исследуем пространство имен System. Secu гity. С ryptog raphy, чтобы научиться применять криптографические службы в приложениях. Не теряя ни минуты, бросимся в волнующий мир криптографии! История криптографии Криптография — это искусство и наука тайнописи (шифрования и дешифрова- ния информации). Термин “криптография” первоначально произошел от двух гре- ческих слов “kryptos” и "graph’', означающих “тайный” и “письмо”. Значительная часть криптографии ориентирована на математику, использует модели и алгорит- мы для шифрования сообщений и других форм коммуникации. Криптография—это ветвь математики, сочетающая в себе исследования в областях криптологии и ' криптоанализа. Криптология - это наука о кодировании и декодировании секретных сообщений, рассматривающая "раскрытие” криптографических систем или дешифрование сообщений при отсутствии предварительного детального знания криптосистемы. Криптоанализ представляет собой обратную сторону криптографии - это наука о взломе кодов, декодировании секретных данных, нарушении схем аутентификации и, вообще, о вскрытии криптографических протоколов. Исторически использование криптографии датируется еще 1900 г. до н. э., когда некий египетский письмописец впервые использовал в коммуникации трансформи- рованные обычные иероглифы того времени. Быстро пролетели несколько тысяче- летий, и в конце 1970-х годов доктор Хорст Фейстель (Horst Feistel) из Исследовательской лаборатории IBM создал предшественника знаменитого алго- ритма DES (Data Encryption Standard). Установленные правительством США жест-
346 Глава 10 кие ограничения раньше препятствовали компаниям США экспортировать криптографические программы. В 1998 г. эти ограничения были ослаблены. Что такое криптография Криптография описывает преобразование открытого или обычного текста в шифрованный текст через процесс, называемый шифрованием. Шифрованный текст преобразуется обратно в открытый или обычный текст обратным процессом, называемым дешифрованием: Шифрование |ф1иф рованный текс Математический криптографический алгоритм, выполняющий преобразования шифрования и дешифрования, также называется шифром, а полученный в результате шифрования текст - шифротекстом. Рассмотрим простой пример, для чего построим страницу ASP.NET. . " г- . «script language= "С#’' runat=*‘server’’> public void Page_,Load(Object sender ,,,EventArgs E) IblHello.Text i“Some information!"; =5*</script>. гй . ш <body style=‘fontj lOpt verdana” bgcblor^fffcc’^ bi *. s <form runat="server"> . <h3>^HackingГ</h3 > .г <asp:Label id='TblHello” Text3"Default text’’ runat3"server” /> </form> .... . . . . . ' </body> Ь ШЭ </html> Эта ASPX-страница только отображает при событии Page_Load() два слова “Some Information!" в серверном элементе управления метки. Не правда ли, простая стра- ница? Посмотрим, как она выглядит в IE 6: 3 http://localhost:81/Hack/Default.AspK ______________ Jp|x| Ste tie* Fflvoribw It ” J Back » Ajoress |.^ bttp //locai 81/Hack/DjfaJt Atpx Я Go Hacking! Some information! Давайте используем инструмент TCPTrace. exe, выполняющий функции анализа- тора пакетов в сети (network-sniffing). С продуктами Windows Server компания Microsoft также поставляет аналогичный инструмент Network Monitor.
Криптография в .NET 347 Инструмент TCPTrace.EXE можно скопировать с сайта С www.PpoketSOAP.com г ’ i - ......._____-_________._.__...... .___,_a<,....... Мой Web-сервер (IIS 5.0) выполняется на порту 80, и, чтобы анализировать паке- ты. отправляемые на мой Web-сервер, изменим в утилите ТСРТгасе порт на 81, тог- да эта утилита будет направлять порту 80 (Web-серверу) все запросы, полученные через порт 81. При доступе к сайту из IE используйте порт 81. Например, если вы об- ращаетесь к localhost на своей машине, то этот доступ надо изменить так: http://localhost:81 / Как можно видеть, любой человек, располагающий простым инструментом ана- лиза пакетов, может читать информацию, которую мы отправляем через открытую сеть. Как же вы можете помешать этой утечке информации? Основную роль в обес- печении безопасности сети играет криптография. Зачем нужна криптография Если ваш компьютер подключен к электронной сети или передает информацию через сеть, то ваши данные видны всем и доступны хакерам. В настоящее время все больше и больше компаний ведут дела через сеть, и это повышает угрозу безопаснос- ти этих компаний, а также взаимодействующих с ними потребителей и партнеров. Чтобы взяться за разрешение этих проблем, все без исключения компании должны предпринять серьезные усилия для защиты своего электронного бизнеса, а также потребителей и партнеров. При надлежащем использовании криптография направлена на решение следую- щих задач: □ Конфиденциальность — гарантирует защиту вашей информации. О Аутентификация — гарантирует, что вы знаете, кто получает доступ к вашей локальной сети.
348 Глава 10 □ Целостность — обеспечивает, чтобы информация не искажалась при пере- сылке. □ Строгое выполнение обязательств (non-repudiation)—гарантирует, что от- правитель не может отрицать отправку сообщения. Криптография обеспечивает все службы, направленные на поддержание безо- пасности и секретности важных данных, передаваемых через открытую сеть. Концепции криптографии Криптографический алгоритм представляет собой математическую функцию, преобразующую сообщение с открытым текстом в нечитаемый текст, имеющий вид “мусора”, обратный процесс порождает читаемый текст из шифрованного сообще- ния. Все криптографические алгоритмы основаны на двух простых принципах: □ Подстановка — эта концепция основана на простой замене каждого символа Сообщения другим символом. Например, при использовании шифра Цезаря (Caesar cipher) каждая буква в сообщении сдвигается вправо (в алфавите) на три. То есть буква “а” становится буквой “d”, “b” превращается в “е” и т. д. Этот алгоритм можно обобщить для любого числа п, не большего 25 (если речь идет о 26-буквенном алгоритме). В данном алгоритме число п является “клю- чом”. Существует много самих разнообразных шифров с подстановкой, в том числе моноалфавитные, полиалфавитные и сложные шифры с подстановкой. □ Перестановка — концепция перестановки базируется на скремблировании символов, присутствующих в сообщении. Некоторые наиболее распростра- ненные формы алгоритмов перестановки связаны с построчной записью сообщения в таблицу и считывания его обратно по столбцам. Некоторые ал- горитмы перестановки, например Triple-DES, для создания шифротекста вы- полняют этот процесс трижды. Математическая формула криптографии проста. Если вы передадите сообще- ние с открытым текстом функции шифрования, она должна создать сообщение CipherMessage, а если вы передадите это CipherMessage функции дешифрования, она должна вернуть первоначальное сообщение: Encryption(Message) = CipherMessage и Decryption(CipherMessage) = Message Точно так же должна быть справедлива следующая формула: Decryption(Encryption(Message)) = Message Поскольку для простого шифрования-дешифрования алгоритм хорошо извес- тен, то каждый, кто знает алгоритм, способен дешифровать CipherMessage. Следо- вательно, для повышения защиты алгоритмов, добавляются ключи. В реальной жизни ключи нужны, чтобы запирать и отпирать замки. Так же шифр является математическим алгоритмом, а ключ к нему - это последовательность байтов, используемая для шифрования и дешифрования информации. Если у нас г ст ключа, мы не можем отпереть замок. Аналогично, потеряв ключ, мы не сможем дешифровать шифрованные данные. Ключи бывают разной длины, которая
Криптография в .NET 349 зависит от криптографического алгоритма. Например, алгоритм DES базируется на 56 битах, а алгоритм RC2 - на 128 битах. Для шифрования или дешифрования сообщения надо передать в функцию соот- ветствующий ключ: Encryption(Message. Key) = CipherMessage и Decryption(CipherMessage, Key) = Message Точно так же должна быть справедлива следующая формула: Decryption(Encryption(Message, Key), Key) = Message Криптографические алгоритмы Криптографические алгоритмы можно подразделить на три типа: □ Симметричные алгоритмы — в симметричных криптографических алгорит- мах для шифрования и дешифрования сообщения используется один и тот же ключ. □ Асимметричные алгоритмы — в асимметричных криптографических ал- горитмах при шифровании и дешифровании применяются разные ключи. Асимметричные криптографические алгоритмы также называются инфра- структурой открытого ключа (public key infrastructure, или PKI). □ Алгоритмы хеширования или дайджеста сообщения — в криптографичес- ких алгоритмах хеширования первоначальный текст преобразуется в шифро- текст фиксированной длины. Алгоритмы хеширования также выполняют одностороннее шифрование, и это означает, что из полученного хеширо- ванием шифротекста нельзя дешифрованием получить первоначальную версию открытого текста. Фиксированная длина шифротекста изменяется в зависимости от алгоритма от 128 до 256 битов. । Симметричные алгоритмы При шифровании и дешифровании информации в симметричных алгоритмах используется, как показано на следующем рисунке, один и тот же ключ: С применением этого метода данные могут передаваться в незащищенную сеть, и получатель дешифрирует информацию, пользуясь тем же криптографическим ал- горитмом, который использовал отправитель. Безусловно, если вы хотите, чтобы
350 Глава 10 этот метод работал надежно, ключ, используемый для шифрования и дешифрова- ния информации, нужно хранить в тайне. Еще одна проблема этого подхода связана с распространением ключа в конечные точки, для чего требуется его шифрование и дешифрование. Более подробную информацию о размерах ключей можно получить из статьи “Selecting Cryptographic Key Sizes ” в файле www. Cryptosavvy. com/Cryptosizes.pdf. К числу наиболее распространенных симметричных алгоритмов относятся сле- дующие алгоритмы: О DES (Data Encryption Standard) — был принят в 1977 г. правительством США, а в 1981 г. — ANSI. В DES применяется 56-битовый ключ для шифрования и дешифрования. Это очень знаменитый алгоритм, но из-за поддержки не- большого размера ключа его использование в современном мире очень огра* ничено. О Triple-DES (или 3DES) — очень надежный алгоритм по сравнению с DES, по- скольку Triple-DES трижды шифрует сообщение, применяя алгоритм DES с разными ключами. Общая длина ключа Triple-DES равна 168 битам. □ Blowfish — это быстрый, компактный и простой алгоритм шифрования, изо- бретенный автором известных книг Брюсом Шнейером (Bruce Schneier), на- писавшим знаменитую книгу “Applied Cryptography” (John Wiley & Sons, ISBN 0471128457). Этот алгоритм допускает использование ключа переменной длины до 448 битов. □ IDEA (International Data Encryption Algorithm) разработали Джеймс Мэсси (James L. Massey) и Суджея Лай (Xuejia Lai) в Швейцарии. Широкому распро- странению этого алгоритма препятствовали отдельные патентные проблемы. □ RC2, RC4, RC5 — алгоритмы RC2 и RC4 были первоначально разработаны Ро- нальдом Ривестом (Ronald Rivest) для компании RSA Security. Оба алгоритма, RC2 и RC4, допускают длину ключа от 1 до 2048 битов. С другой стороны, RC5 позволяет пользователю определять длину ключа, размер блока данных и чис- ло циклов шифрования. □ Rijndael (AES) — этот алгоритм первоначально разработали Джоай Димен (Joan Daemen) и Винсент Риджмен (Vincent Rijmen). Rijndael—быстрый, ком- пактный алгоритм, поддерживающий ключи длиной 128,192 и 256 битов. Среда .NET Framework поддерживает симметричные алгоритмы шифрования DES, Triple-DES, RC2 и Rijndael. Алгоритмы с симметричными ключами работают гораздо быстрее алгоритмов PKI, поэтому для шифрования и дешифрования больших блоков данных они пред- почтительнее. Кроме того, их очень легко реализовать. Основной недостаток сим- метричного шифрования состоит в том, что нужно защищать ключи, и обмен ключами между шифрующим источником и дешифрующим назначением всегда представляет проблему. Безопасность алгоритмов также связана с длиной ключа: чем длиннее ключ, тем меньше вероятность дешифрования информации. Вероят- ность дешифрования информации также базируется на сложности используемого ключа. Чем сложнее ключ, тем менее вероятно дешифрование информации.
Криптография в .NET 351 Асимметричные алгоритмы В асимметричных алгоритмах один ключ шифрует сообщение, а другой дешиф- рует полученную информацию, как показано на следующем рисунке: Здесь применяются разные, но связанные между собой ключи. Следовательно, мы можем публиковать открытый ключ, не опасаясь, что наши возможности шиф- рования данных будут скомпрометированы. Такое шифрование также называется шифрованием с открытым ключом, или Public Key Infrastructure (PKI), посколь- ку доступность открытого ключа не дискредитирует целостность и безопасность ключа или сообщения. Ключ дешифрования обычно называют закрытым или сек- ретным ключом. Криптография открытого ключа и связанные с ней стандарты и технологии ле- жат в основе средств безопасности многих продуктов, включая подписанную и шиф- рованную электронную почту, подписание форм, подписание объектов, однократное предъявление пароля и наиболее популярный протокол Secure Sockets Layer (SSL)/TLS. Как показано на предыдущем рисунке, открытый ключ может распространяться свободно, но только вы можете прочитать сообщение, зашифрованное этим клю- чом. Когда кто-либо отправляет вам сообщение, он шифрует его вашим открытым ключом, а вы, получив это сообщение, дешифрируете его соответствующим закры- тым ключом. Сообщение, зашифрованное открытым ключом, можно дешифровать только соответствующим закрытым ключом. Системы PKI также используют методы обмена ключами, например, “метод Диффи-Хеллмана (Diffie-Hellman)". Это не алгоритм, а метод, позволяющий разработать безопасное средство обмена ключами между двумя сторонами. Также используются методы Digital Signature Standard (или DSS)h Elliptic CurveCryptosystems. i Еще один способ состоит в копировании открытого ключа из онлайнового репо- зитория. Например, если вы получили сертификат клиента из компании Thawte, можно внести ваш открытый ключ в такой репозиторий. Если кому-либо потребует- ся отправить вам нечто очень секретное, ему нужно только получить ваш открытый ключ у вас или из онлайнового репозитория, зашифровать информацию этим клю- чом и отправить ее вам. Получив сообщение, вы дешифруете ее вашим закрытым ключом. Это один из наиболее распространенных способов обмена ключами. До сравнению с шифрованием симметричным ключом шифрование открытым ключом требует выполнить больше вычислений и, следовательно, не всегда целесо- образно для больших объемов данных.
352 Глава 10 В среде .NET Framework поддерживаются два асимметричных алгоритма: О DSA/DSS — Digital Signature Standard (DSS) (стандарт цифровой подписи) разработан Национальным агентством по безопасности (National Security Agency). DSS основан на алгоритме цифровой подписи (Digital Signature Algorithm, DSA) и поддерживает любую длину ключа. □ RSA—это хорошо известный алгоритм открытого ключа, разработанный Ро- нальдом Ривестом, Ади Шамиром (Adi Shamir) и Леонардом Адлеманом (Leonard Adleman), поддерживают переменную длину ключа, которая зави- сит от конкретной реализации. Алгоритмы дайджеста сообщения Алгоритмы дайджеста сообщения (Message Digest Algorithms, также называемые МАС или хеш-алгоритмами) преобразуют входные данные переменного размера в строку фиксированной длины, как показано на рисунке: Хеш-алгоритмы также называют односторонним хешем, поскольку хеширован- ную строку нельзя преобразовать обратно в первоначальное состояние. Как только исходный открытый текст хеширован, получить из МАС первоначальное значение невозможно. Когда хешируется открытый текст, алгоритм генерирует значение, используя некий ключ. Получив хешированное значение, можно отправить открытый текст и хеш-значение другой стороне, где открытое текстовое значение можно хеширо- вать тем же ключом и алгоритмом. Сгенерированное хеш-значение можно затем сравнить с тем, которое было получено от другой стороны. Если значения совпада- ют, можно быть уверенным, что сообщение не изменилось при пересылке. Модуль ASP.NET, выполняющий аутентификацию на основе форм, поддер- живает односторонний хеш-алгоритм MD5 или SHA3 для подтверждения подлинности имен пользователей и паролей по данным, хранимым в файле i conf 1о. web. Эту функциональность можно расширить, сохранив хеширо- ванные пароли в базе данных методом HashPasswordForStoringlnConfigFile класса Forms Au then tic at ion, входящего в пространство имен System. Web, Security, Более подробно об этом можно прочитать в книге “Professional ASP.NET Security” (Wrox Press, ISBN: 1-86100-620-9). "И К распространенным МАС-функциям относятся MD2, MD4, MD5, SHA, SHA-1, SHA-256, SHA-384 и SHA-512. Среда .NET Framework поддерживает следующие хеш-функции:
Криптография в .NET 353 □ HMACSHA-1 □ MACTripleDES □ MD-5 □ SHA-1 □ SHA-256 □ SHA-884 □ SHA-512 Функция Hash Message Authentication Code (или HMAC) позволяет проверить целостность сообщения, переданного между двумя сторонами, которые договорились совместно использовать один ключ. Цифровые подписи Хотя шифрование и дешифрование решают нескольких задач, они не затрагива- ют две важные проблемы: □ Фальсификация □ Имитация Цифровые подписи используют односторонние хеш-функции для обнаружения подделок и близких к этому проблем аутентификации. Поскольку хеш-значение уни- кально для хешированных данных, любые изменения в данных, даже удаление или изменение единственного символа, приводят к генерации другого значения. Более того, содержание хешированных данных практически никак нельзя получить из хеша. Таким образом, этот прием становится наилучшим для обнаружения фальси- фикации. В шифровании открытым ключом можно использовать закрытый ключ для шифрования и открытый ключ для дешифрования. Поскольку это могло бы создать проблемы при шифровании секретной информации, мы можем снабжать цифро- вой подписью любые данные, вместо того чтобы их шифровать. Подписание дан- ных создает одностороннее хеш-значение для данных, которые затем можно дешифровать, используя закрытый ключ. Шифрованное хеш-значение вместе с дру- гой информацией, в том числе алгоритмом хеширования, известны как цифровая подпись. На следующем рисунке показано упрощенное представление способа ис- пользования цифровой подписи для проверки целостности подписанных данных:
354 I Глава 10 Как видно на рисунке, отправитель посылает сообщение с открытым текстом (или шифрованное) и цифровой подписью. Цифровая подпись вычисляется на основании сообщения открытого текста, сообщение с открытым текстом хеширует- ся с использованием такого алгоритма, как MD5, и хешированное значение будет подписано закрытым ключрм. На другой стороне мы получим открытое (или шиф- рованное) сообщение с цифровой подписью. Затем вычисляем хеш-значение для открытого текста и сравниваем его с цифровой подписью. Если процесс проверки цифровой подписи оказался успешным, убеждаемся, что при пересылке данные не были фальсифицированы. Криптографическая терминология Прежде чем погружаться в мир криптокодирования, нужно разобраться с неко- торой криптографической терминологией. Блочные и поточные шифры Криптографические шифры обрабатывают данные в двух форматах: □ Блочные шифры (block ciphers) □ Поточные шифры (stream cipher) Традиционно блочные шифры более популярны. Блочный шифр преобразует блок фиксированной длины открытого текста в блок шифрованных данных той же длины и повторяет этот процесс, пока не обработает все сообщение. Это преобра- зование происходит под действием предоставленного пользователем секретного ключа. Дешифрование выполняется применением обратного преобразования к блоку шифротекста с использованием того же самого секретного ключа. Фиксиро- ванная длина называется размером блока, и для многих блочных шифров размер блока равен 64 битам. Обычно симметричные алгоритмы базируются на формате блочного шифра. Например, алгоритмы DES и RC2 используют 8 байтов, 3DES — 16 байтов, a Rijndael — 32 байта входных данных. Используя эту шкалу, каждый алго- ритм ддлит входную информацию на блоки и выполняет соответствующее преобра- зование. . Более современные симметричные алгоритмы шифрования базируются на по- точных шифрах. Каждый поточный шифр генерирует поток ключа (keystream) и выполняет шифрование, комбинируя этот поток с открытым текстом (обычно применяя побитовую операцию XOR). Поточные шифры можно спроектировать так, чтобы они работали очень быстро, гораздо быстрее, чем блочные шифры. В то время как блочные шифры действуют на больших блоках данных, поточные шифры > обычно оперируют с меньшими порциями открытого текста, обычно битами. Шиф- рование любого конкретного открытого текста блочным шифром при использова- нии одного и того же ключа приводит к получению одного и того же шифротекста. Применяя поточный шифр, можно получать разные преобразования этих более мелких порций текста в зависимости от того, в какой момент в процессе шифрова- ния они встречаются. , Заполнение Блочные шифры имеют дело с блоками битов (обычно по 64 бита), и последние оставшиеся биты могут не соответствовать шифрующему блоку. Например, предпо- ложим, что у нас есть 136 битов информаци) [ которые мы пытаемся зашифровать с помощью блочного шифра, принимающего для шифрования 64 бита (или 8 бай-
Криптография в .NET 355 тов). На следующем рисунке показано, как 136-битовая входная информация разби- вает. ся на 64-битовые блоки для процесса заполнения (padding). 13С о*лоь Ь uwrob В процессе шифрования первые два блока будут содержать 128 битов, а оставши- еся восемь битов не будут соответствовать буферу блочного шифра. Чтобы обрабо- тать неполный блок, требуется его заполнить. Схема заполнения определит, как в процессе шифрования будет обрабатываться последний незаполненный блок дан- ных. В процессе дешифрования снова вызывается схема заполнения, которая удаля- ет все лишние символы и восстанавливает исходный текст. Стандарт криптографии открытого ключа (PKCS#5, или Public Key Cryptography Standard) - это одна из наиболее известных схем заполнения, опубликованная компа- нией RSA Security, Inc. Чтобы узнать о ней подробнее, посетите Web-сайт RSA http://www.rsa.com/rsclabs/pubs/PKCS/. Среда .NET Framework обрабатывает заполнение, используя перечисление PaddingMode. В нем поддерживается три значения: * □ None □ PKCS7 □ Zeros Как следует из названия, когда используется значение None, заполнение не вы- полняется. Если мы выбираем PKCS7, остающееся число блоков будет заполнено остающимся числом байтов. Например, если остались свободными шесть байтов, пос хедние шесть байтов будут заполнены значением шесть. Если свободны четыре байта, последние четыре байта будут заполнены числом четыре: Когда используется значение PaddingMode. Zeros, оставшиеся байты будут запол- нены нулями: Режимы Режим шифра определяет, как блоки открытого текста будут шифроваться в бло- ки шифротекста и как будет выполняться обратное дешифрование. Перечисление CipherMode определяет режим блочного шифрования, используемый при выполне- нии процесса шифрования или дешифрования. Можно задать свойство Mode не- скольких алгоритмов шифрования. Среда .NET Framework поддерживает режимы СВС, CFB, CTS, ЕСВ и OFB. При использовании режима электронной кодовой книги (Electronic Code Book, или ЕСВ) каждый блок открытого текста преобразуется в блок шифротекста. Основной недостаток режима ЕСВ заключается в том, что один и тот же открытый
356 Глава 10 текст всегда шифруется в один и тот же шифротекст, если используется тот же ключ. Режим сцепления шифрованных блоков (Cipher Block Chaining, или СВС) устра- няет недостатки режима ЕСВ. Когда используется режим СВС, каждый блок откры- того текста комбинируется с шифротекстом предыдущего блока (при помощи операции XOR operation), и создаются зашифрованные блоки шифротекста: Поскольку в начале этого процесса не существует предыдущего шифрованного блока, то первый блок открытого текста заменяет вектор инициализации (IV). В режиме обратной связи шифра (Cipher Feedback, или CFB) блочный шифр действует как поточный шифр, небольшими приращениями преобразуя открытый текст в шифротекст, вместо того чтобы обрабатывать данные по блокам. Режим CFB также использует IV, чтобы обработать первоначальный открытый текст. Режим захвата шифротекста (Cipher Text Stealing, или CTS) — это очень гибкий режим, чье поведение похоже на режим СВС. Режим CTS обрабатывает открытый текст любой длины, производя шифротекст, соответствующий длине открытого текста. Режим обратной связи вывода (Output Feedback, или OFB) работает почти так же, как режим CFB. Единственное отличие состоит в способе обработки внутренне- го буфера (сдвигового регистра). Пространство имен Sy stem.Security. Cryptography Пространство имен System.Security.Cryptography обеспечивает простой способ реализации средств безопасности B.NET-приложении при помощи криптографи- ческих классов. Некоторые криптографические классы представляют собой явные управляемые коды .NET, а некоторые из них являются оболочками для неуправляе- мого Microsoft Crypto 4PI. Вы можете понять, какой, управляемый или неуправ- ляемый, код заключен в криптографическом классе по его имени. Имена всех неуправляемых провайдеров заканчиваются суффиксом С ryptoSe rviceP rovide г, а все управляемые провайдеры названы именами, имеющими суффикс Managed. Напри- мер, если вы посмотрите на классы хеширования, среди которых есть MD5CryptoSeг- viceProvider, SHAIManaged и SHA256Managed, то сможете понять, что классы SHAIManaged и SHA256Managed — это чисто управляемые .NET-реализации алгоритма SHA. Crypto API- это API компании Microsoft, предназначенный для доступа к криптографическим функциям, встроенным в платформу Windows. Недавно Microsoft выпустили CAPICOM, ActiveX-оболочку вокруг Crypto API, упрощающую программирование на Crypto API в Visual Basic 6, но в ней реализовано только подмножество этого API. Заметим, что .NET Framework поддерживает использование строгой длины клю- ча во всех алгоритмах шифрования. Однако для алгоритмов шифрования, реализо-
Криптография в .NET 357 ванных поверх Crypto API, нужно усовершенствовать версию Windows и установить High Encryption Pack: □ Для пользователей Windows 2000 High Encryption Pack включен в Service Pack 2. Его можно получить со следующего URL: http://www.microsoft.com/windows2000/clownldads/recommended/encryption/. □ Для пользователей Windows NT 4.0 High Encryption Pack включен в Service Pack 6a; его можно скопировать co страницы http://www.microsoft.com/ntserver/nts/downloads/recommended/SP6/allSP6.asp. □ Для пользователей Windows ME, Windows 98 и Windows 95 High Encryption Pack содержится в Internet Explorer 5.5, его также можно скопировать со стра- ницы http://www.microsoft.com/windows/ie/download/128bit/default.asp. Иерархия криптографии классов Пространство имен System Security. Cryptography содержит три класса высокого уровня, SymmetricAlgorithm, AsymmetricAlgorithm и HashAlgorithm, представляющие три основные области криптографии (см. схему): System.Security.Cryptography.SymmetricAigorithm I System.Seci jj, System.Seci ; System.Seci System.Seci ► System.Secunty.Cryptography.AsymmetncAlgorith' r ' System.Secuffty.Crypt6grapny.DSA System.Security.Cryptography.RSA 1 System.Se>.uhty.C/yp<ography.HashAlgonthm "Г ' । Syste ruin. ,WW.I I 1 11 System.Secuhty.Crypiography.MD5 System.Secunty.Cryptography.SHAl ►i System.Security.Cryptography.SHA256 System.Security.Cryptography.SHA384 System.Secuhty.Cryptography.SHA512 - ---— - -fl- — ... "MM Л* System.Secu.hty.C'yptography.SHA512 ЧЙ» • •• и Эта модель дает гибкость в расширении пространства имен. Например, про- странство имен System.Security.Cryptography не поддерживает в настоящее время симметричный алгоритм Blowfish. Если бы мы захотели добавить его в это про- странство имен, то потребовалось бы только создать класс алгоритма Blowfish, про-
358 Глава 10 изводный от класса SymmetricAlgorithm, и мы получили бы для него совершенно бесплатно значительную часть обычных функциональных возможностей. Еще одно преимущество состоит в том, что все классы провайдеров алгоритмов наследуются от классов реализации этих алгоритмов. Например, провайдер алгоритма хеширо- вания SHA1 (класс SHA1 Managed) порожден от реализации алгоритмов хеширования SHA1 (класса SHA1). Хеширование в .NET Пространство имен System. Security. Cryptography в .NET Framework обеспечива- ет несколько интерфейсов, называемых Cryptographic Service Providers (CSP), которые реализуют ряд алгоритмов хеширования и упрощают применение хеширо- вания. Как было показано, в среде .NET Framework реализованы несколько хорошо из- вестных, обеспечивающих защиту алгоритмов хеширования, в том числе Message Digest 5 (MD5) и Secure Hash Algorithm (SHA). Провайдер MD5 генерирует 128-бито- вые хеш-значения, а провайдер SHA может формировать 160, 256, 384 и 512-би- товые хеш-значения. Метод ComputeHash(), входящий в оба CSP (MD5 и SHA), принимает массив байтов или объект Stream и возвращает хеш-значение. В .NET Framework также реализовано хеширование, базирующееся на ключе, которое часто используется для генерации цифровых подписей. Класс HashAlgorithm Все классы алгоритмов хеширования наследуют абстрактному классу HashAlgorithm. Класс HashAlgorithm предлагает несколько общих методов и свойств, которые можно применять для всех алгоритмов хеширования. Некоторые их них рассматриваются в таблице: Член класса Описание Hash и HashValue / Возвращают вычисленное хеш-значение в массиве байтов Hash — открытое свойство, a HashValue — защищенное поле; оба они возвращают массив байтов. HashSize и HashSizeValue Возвращают размер хеш-значения в битах. HashSize является открытым свойством, a HashSizeValue —* защищенным полем, оба они возвращают целое значение. InputBlockSize Открытое свойство, возвращающее целое число, которое представляет размер входного блока в битах OutputBlockSize Открытое свойство, возвращающее целое число, которое представляет размер выходного блока в битах. ComputeHash() Вычисляет хеш-значение для заданного входного массива байтов или объекта St ream. ComputeHash () — это открытый метод, помещающий выходное значение в массив байтов или объект St ream. Create() Создает экземпляр хеш-алгоритма, используемого в настоящий момент. Например, воспользовавшись алгоритмом MD5, можно создать объект с типом этого алгоритма. С reate () — статический метод.
Криптография в .NET 359 продолжение таблицы Tr^nsformBlockO Т ransformFinalBlock() I State Генерирует хеш-значение для заданного входного массива байтов и копирует результат другой массив байтов. ТransfогтВlocк() — это открытый метод, возвращающий массив байтов. Генерирует хеш-значение для заданной совокупности и возвращает массив байтов. Тransf ormFinalBlock() — это открытый метод, возвращающий массив байтов. Возвращает состояние вычисления хеш-значения. Это значение будет содержать нуль перед вычислениями и ненулевое значение после успешного завершения вычисления хеш-значения. State — защищенное поле, возвращающее целое значение, представляющее текущее состояние вычисления хеш-значения. Рассмотрим простой пример вычисления MD5-xema. Этот метод принимает мас- сив байтов (открытый текст) и возвращает массив байтов (хеш-значение): byte [] ComputeMD5(byte □ inputs ’ WS J MD5CryptoServiceProvider mdbProvider =? new MD5CryptoServtceProvider(); return md5Provider. ComputeHash(input) ; > , м }<«• % « "" Как можно видеть, использовать провайдер MD5 очень легко. Нужно только со- здать новый объект типа MD5CryptoSe rviceProvide г и передать массив байтов методу ComputeHash(). Этот же прием можно применять для всех алгоритмов хеширования, доступных в среде .NET Framework. Напишем простое Windows-приложение, пре- образующее строку с помощью нескольких алгоритмов хеширования. Вот оно в действии: Реализовать это приложение довольно просто. Мы принимаем от пользователя строку и хешируем ее разными алгоритмами. Так выглядит код для обработчика щелчка по кнопке Compute Hash: private void btnCompute Click(object sender, System.EventArgs e) if (txrHash.Text.TrlmO !.= .. f . V ‘ «. x- . Л ^-&А'А // Генерируем массив байтов по введенной строке
360 Глава 10 byte[] inputDuta - ASCHEncoding ASCII GetBytes(txtHash.Te<t); •Sr ’***& ;j “ * <>- V- ’ x' ( «'s ‘ »<_>. _ 7/ Отображаем хеш-значения в текстовых поля? 1 txtnO5 ;ext = ASCHEncoding ASCII, Get Strings new MD5CryptoSen-ioeProVider() ComputeHash(inputData)); txtSHAl.Text = ASCHEncoding.ASCII GetString(new SHA1Managed().ComputeHash(inputData)) ; xtSHA256.Text * ASCHEncoding.ASCII GetSt ring (pew SHA256Managed().ComputeHash(inputData)); txtSHA384 Text = ASCII EncodingASCU GetSt ring «'new > SHA384Managed() ComputeHash(InputData)); txtSHA512,Text я. ASCHEncoding. ASCII. GetSt ring (new SHA512Hanaged().ComputeHash(inputData)); V В ; } Во-первых, чтобы преобразовать строковую переменную в массив байтов, вызы- ваем метод GetBytes() класса ASCHEncoding. Затем создаем новый объект для каждого алгоритма и передаем входной массив байтов методу ComputeHash (). Далее вызываем метод GetSt ring() того же класса ASCHEncoding и преобразуем массив байтов в стро- ку. Вот как просто. И если захотите увидеть этот код в работе, не забудьте добавить директивы using для пространств имен System.Text и System. Security. Cryptography. | « Если вы не имеете дело со строками ASCII, можно использовать класс UnicodeEgcoding лрострадства System. Text. v 1 . .. Использование хеш-значений для аутентификации Приемы хеширования очень полезны, когда нужно идентифицировать пользо- вателей. Мы собираемся познакомиться с простым методом аутентификации для Windows-приложения, использующим алгоритм MD5. В целях иллюстрации имя пользователя и пароль будут распознаваться по базе данных Access. Мы создадим базу данных Access с именем WroxDBAuth.mdb с одной таблицей Tbl_MA_Users. Как показано на следующем рисунке, в этой таблице будем хранить идентификатор пользователя, адрес электронной почты, пароль, имя и фамилию: И Tbl_MA_Users: Table | Г"' "aLE7‘‘J Гчка Type ISH UserID Number L_ . . I flLi Email Text Length -100 |Pw? Text length -192 Iе iFrstName Text Length-50 I llestName Text Length-SO Пусть адрес электронной почты будет именем входа в приложение для пользова- телей, а пароль хранится в базе данных в хеш-значении, полученном через MD5. niSsivakumar@chennai.net ?DH!8DXJ~DNI,HDS Srinivasa Sivakumar
Криптография в .NET 361 Это охраняет пользователей от нападений хакеров на их пароли. Например, пароль “MyPass”, хранящийся в базе данных в хеш-формате MD5, имеет вид " {? • Н! 8eXJ~’N”H*S". Построим простой экран аутентификации, чтобы идентифициро- вать пользователей по базе данных Access: Далее приводится код для кнопки Login: private void btnLogin_Click(object sender, System.EventArgs e> if (txtEmail.Text.Trim() I* "" && txtPwd.Text.Trim() ! = "") authenticateUserO ; } Во-первых, проверим, что в текстовые поля для имени пользователя и пароля что-то введено. Если это так, вызываем метод authenticateUserO, в котором устанав- ливаем соединение с базой данных Access и получаем из таблицы строку с введен- ным именем пользователя. private bool authenticateUserO { bool bRtnValue,= false; .. , j string strConn = “PROVIDER-Microsoft.Jet.0LEDB.4.0;* + "DATA SOURCE=c:\\DB\\WroxDBAuth.mdb; OleDbConnection Conn = new OleDbConnection(strConn); Conn.OpenО; String strSQL = "SELECT Pwd FROM Tbl_MA_Users WHERE Email = + txtEmaiLText + OleDbCommand Cmo = new 01eDbCommand(strSQL, Conn); // Создаем объект соединения datareader OleDbDataReader Dr = Cmd ExecuteReader( System.Data.CommandBehavior.CloseConnection); // Получаем первую строку и проверяем пароль. if (Dr. ReadO) { Далее, передаем пароль, введенный пользователем открытым текстом, в метод Gene rat eMD5Hash(), возвращающий хешированную строку. Если текущий хеширован- ный пароль, хранящийся в базе данных, совпадает с хеш-значением, сгенерирован- ным методом Gene rateMD5Hash (), мы отображаем сообщение Password was successful! Иначе отображаем сообщение Invalid password: if (Dr(“Pwd”J.ToStringO == GenerateMD5Hash(txtPwd.Text)) MessageBox.Show(this, “Password was successful!”) bRtnValue •'= true; £ —< - t t
362 Глава 10 ?1S® Л'Х * • ' • “ЧМ «Ju >• I ( *• • Й? . MessageBox.Show(this,’Invalid passwoid ’), “Жй?г * Лк ., '-' • '' MwSfW- 9 . . £. V- 'гЛ:х<- } else J ' ' l s' MessageBox Shpw(this, “Login name not found.”); J- fi’i-, t» -j I; , f ** , ' r *»-- , •*« W •; T - 3--. $ :.-.s;Ax - Dr CloseO; return bRtnValue; • } Написать метод GenerateMD5Hash() совсем нетрудно. Сначала преобразуем вход- ную строку в массив байтов, используя класс ASCIIEncoding. Затем создаем новый объект типа MD5CryptoSe rviceProvide г и вызываем его метод ComputeHash (), генериру- ющий хеш-значение. Наконец преобразуем хеш-значение в строку и возвращаем вы- зывающему коду: j st ring Gene rateMfnHast string moot) Л • • // Создаем массив байтов по входной строке byte[] inputData - ASCIIEncoding.ASCII.GetBytes(input); // Вичиоляем хеш-значение MD5 Mp5 md5Provider - new MD5CryptoServiceProvider(); 6yte[l nashResult = md5Provjder.ComputeHash{JnputBb‘'a«; return ASCIIEncoding,ASCII.GetString(hsshResult); } Эта процедура проста в реализации и дает превосходную модель безопасности для приложений. В последнем примере использовался алгоритм MD5. Точно так же при реализации этого приложения мог быть применен SHA1 или любой другой ал- горитм хеширования. В таком подходе заключена одна проблема - если пользователь *захочфг . получить пароль по электронной почте, мы не сможем его прислать, поскольку нс сумеем преобразовать хеш-зна жение обратно в открытый текст. Однако всегда можно отбросить старое значение и отправить пользователю новый пароль, г ч . Хеш-значения, снабженные ключами Алгоритмы хеширования с ключами, или НМАС (Hash Message Authentication Code), очень похожи на обычные алгоритмы хеширования, за исключением того, что они генерируют хеш-значения на основании ключа. Алгоритмы НМАС можно использовать так же, как алгоритмы хеширования. Например, с помощью НМАС-значения можно проверить целостность сообщения, передаваемого между двумя сторонами, договорившимися об общем секретном ключе. Это напоминает симметричный алгоритм. Для вычисления хеш-значения НМАС объединяет исходное сообщение с клю- чом. Отправитель вычисляет по открытому тексту НМАС-значсние и отправляет его вместе с открытым текстом. Получатель заново определяет НМАС, используя
Криптография в .NET 363 открытый текст и копию ключа отправителя. Если вычисленное значение НМАС совпадает с отправленным другой стороной, тогда получатель понимает, что исход- ное сообщение не было изменено, поскольку его дайджест не изменился. Таким об- раз зм, получатель может проверять аутентичность переданной информации. Обычно НМАС-значения используются для цифровой подписи. В среде .NET Framework поддерживаются алгоритмы HMACSHA1 и MACTripleDES. Алгоритм HMACSHA1 вычисляет хеш-значения, используя алгоритм SHA1, а МАСТriple- DES базируется на алгоритме Triple-DES. Для простого примера с использованием классов НМАС построим Windows-приложение, показывающее НМАС-значение для заданного открытого текста и заданного ключа. В первую очередь преобразуем входные строки текста и ключа в массивы бай- тов. После этого создаем объект типа HMACSHA1 и передаем его в объект CryptoSt ream. Затем, применяя обычные потоковые операции, считываем входной массив и за- крываем поток. Свойство Hash объекта HMACSHA1 возвращает значение НМАС. Тот же самый процесс повторяется для алгоритма МАСТ г ipleDES: void ProcessKeyedHash(string input, string key) try { // Генерируем массивы байтов для входных строк byte[] inputData = ASCHEncoding.ASCII.GetBytes(input); byte[] keyBytes = new byte[16]; keyBytes - ASCHEncoding. ASCII. GetBytes(kpy); // Вычисляем HMACSHA1 HMACSHA1 hmac = new HMACSHAK keyBytes); CryptoStream cs * new CryptoStream(Stream.Null, hmac, CryptoStreamMode. Write);; cs.Write(inputData, 0, inputData.Length) ; cs.CloseO; • । txtHMACSHAI. Text = ASCHEncoding .ASCII. GetSt ring(hmac. Hash); // Вычисляем MACTripleDES MACTripleDES macTripleDES = new MACTripleDES(keyBytes); txtMACTripleDES.Text = ASCHEncoding. ASCII. GetString( } ' macTripleDES.ComputeHash(inputData)); catch (Exception e) MessageBox.Show (this, e.ToStrlng());
364 Глава 10 Так выглядит это приложение в действии: Между этими двумя алгоритмами есть единственное отличие. Алгоритм HMACSHA1 принимает ключи любого размера и производит хеш-значение длиной 20 байтов. С другой стороны, алгоритм МАСТ г ipleDES использует ключи длиной 8,16 или 24 бай- та и генерирует хеш-значение длиной восемь байтов. Если длина ключа отличается от необходимой длины, порождается исключение. Симметрические преобразования в .NET Пространство имен System.Security.Cryptography поддерживает симметричес- кие алгоритмы DES, Triple-DES, RC2 и Rijndael. В этом перечне только у алгоритма Rijndael есть управляемая реализация, остальные алгоритмы пользуются своими аналогами в Microsoft Crypto API. Класс SymmetricAlgorithm Все классы симметричных алгоритмов наследуют классу SymmetricAlgorithm. Он представляет несколько общих методов и свойств, которыми можно пользоваться во всех алгоритмах хеширования. В следующей таблице рассматриваются некото- рые из этих свойств и методов: ( Член класса Key и KeyValue KeySize и KeySizeValue LegalKeySizes и LegalKeySizesValue Описание Задают для симметричного алгоритма секретный ключ. Key — это открытое свойство. KeyValue — защищенное поле, оба свойства возвращают массив байтов. Задают размер секретного ключа в битах. KeySize — это открытое свойство. KeySizeValue —защищенное поле, и оба свойства возвращают целое значение, представляющее длину ключа в битах Задают для данного симметричного алгоритма корректные размеры ключей в байтах. LegalKeySizes — это открытой свойство, LegalKeySizesValue — защищенное поле, оба свойства возвращают массив KeySize.
Криптография в .NET 365 продолжение таблицы IV и IWalue Задают вектор инициализации для симметричного алгоритма. IV — это открытое свойство, a IWalue защищенное поле, оба свойства возвращают массив байтов. BlockSize и BlockSizeValue Задают размер блока в битах для текущего симметричного алгоритма. BlockSize — это открытое свойство, a BlockSizeValue —защищенное поле, и оба свойства возвращают целое число. LegalBlockSizes и , LegalBlockSizesValue Задают допустимые размеры блока, поддерживаемые текущим симметричным алгоритмом. LegalBlockSizes — это открытое свойство, a LegalBlockSizesValue — защищенное поле, и оба свойства возвращают массив KeySize. Mode и ModeValue Задают режим симметричных операций, используемых текущим алгоритмом. Mode — это открытое свойство, ModeValue — защищенное поле, и оба возвращают CipherMode Padding и PaddingValue Задают режим заполнения, используемый текущим симметричным алгоритмом. Padding —это открытое свойство, PaddingValue — защищенное поле, и оба возвращают PaddingMode. CreateEncryptorO Метод CreateEncryptor() создает объект симметричного шифрования, используя указанные ключ и вектор инициализации. CreateEncryptor() — это открытый метод, возвращающий интерфейс ICryptoTransform. CreateDecryptorO Метод CreateDecryptorO создает объект симметрического дешифрования, используя ключ и вектор инициализации. * CreateDecryptor() — открытый метод, возвращающий интерфейс ICryptoTransform. GenerateKeyO Метод GenerateKeyO генерирует для симметричного алгоритма случайный ключ и заменяет им значение, хранящееся в свойстве Key GenerateKeyC) — открытый метод, возвращающий в массиве байтов случайный ключ. GeneratelVO Метод GenerateIV() генерирует для симметричного алгоритма случайный вектор инициализации и подменяет им значение, хранящееся в свойстве IV. GenerateIV() — открытый метод, возвращающий случайный вектор в массиве байтов. ValidKeySizeO Показывает, допустим ли указанный размер ключа для текущего симметричного алгоритма. ValidKeySize() — открытый метод, возвращающий целое число. Начнем исследование симметричных алгоритмов с алгоритма DES. Поскольку симметричные алгоритмы обычно быстрее асимметричных, они хорошо подходят для объемных операций шифрования-дешифрования, например, для операций с файлами. Напишем Windows-приложение, которое будет шифровать и дешифро- вать файлы с использованием алгоритма DES. Пользовательский интерфейс будет предоставлять возможности задавать фай- лы в элементах управления общего диалога Windows. Эти файлы будут шифроваться и дешифроваться с использованием секретного ключа.
366 Глава 10 Кнопка Encrypt будет добавлять к имени исходного файла расширение . епс и ге- нерировать шифрованный результирующий файл. Вот как выглядит код для кнопки Encrypt. privatevoidbuttonl_Click(object sender, System. Event Args e) { “ A' : ‘ if (encryptData(encFile.Text, encFile.Text + ".eno”; txtKe, .Text) -ад -true) MessageBox. Show(this, “Done! ”, “Encryption Status”, Mes; ageBoxButtons OK, MessageBoxIcon.Information); -- else .=. ‘r, ./ .. '*? >• p ............................ MessageBox.Show(this, “The meryption process failed!”, “Fatal Error”. MessageBoxButtons.OK, MessapeBoxIcon.Stop); * •* j \' ’ ' ' " • • ' - - . Событие щелчка по кнопке Encrypt вызывает метод епс ryptData(), передавая ему имя исходного файла, имя результирующего файла и секретный ключ для шифрова- ния. Рассмотрим этот метод. Сначала создаем объект типа DESC ryptoSe rviceProvide г и назначаем секретный ключ, предъявленный пользователем, свойству Key. Затем вызываем метод Gene rateIV(), генерирующий вектор инициализации для операции шифрования. Да- лее, вызывая метод CreateEncryptorO класса DESCryptoServiceProvidfcг, создаем объ- ект шифрования DES. После этого создаем два объекта FileStream, один в режиме чтения (исходный файл), другой в режиме записи (результирующий файл) для шиф- рования: . // Метод encryotData шифрует заданный файл, лспользуя алгоритм DES public bool encryptDataCstring sourceFile, string destination File, „ - string cryptoKey) - try // Создаем провайдер сервиса DFS, назначая кдю1 и вектор DESCryptoServiceProvideг BESProvxder ₽ new DESCryptoServiceProvider(); , DESProvider Key = ASCIIEncoding ASCI^.GetBytes(cryptoKey); DESProvidpr GereiatdlVO; ICryptoTransform DESEncrypt ? DESProvider.CreateEncryptorO; // Открываем исходный и результирующий файлы, используя Файловый поток FileStream inEileStream = new FileStream(sourceFile, FileMode.Open, FileAccess Read); FileStream. outFileStream = new FileStream(destinationFile, > FileMode Create, FileAccess.Ote): r Вектор инициализации (или IV) всегда используется для инициализации перед шифрованием первого блока открытого текста. Это было обсуждено в режиме Modes.
Криптография в .NET , ,......... __ .... _ 367 После создания этих объектов потребуется объект CryptoSt ream, в который запи- шем шифрованный файл. В конструктор CryptoSt ream передаем объект DES-шифро- вания и выходной файловый поток. Затем считываем содержание входного файла и записываем его в CryptoSt ream. Наконец закрываем все потоковые объекты и возвра- щаем значение true: « й 7/ Создаем экземпляр Ci ytoStream для записи шифрованного файла " : CryptoStream CryproStream = new CryptoStream(outFileStream, DESEncrypt, CryptoStreamMode.Write); \ _♦ x - о ’й . / Л ’ * x Объявляем массив байтов с длиной «гдного файла byte[] byteamayinput « newbyte[inFileSrream.Length - 1]; 7/ Считываем входной файловый поток в массив байтов и < // записываем его в CryptoStream InFileStream. Readgbytesг rayinput, . 0, bytea •'rayinput. Length); CryptoStream.Write(bytearrayinput, 0, bytearray input. Length) ; 11 Закрываем обработ чики потоков cryptoStream.Closet); < Л InFileSt ream. CloseQ;. outFileSt ream. C'.ose() ; return true; s } catch (Exception e) v- { ' **' *** ы . Message Box. Show(this. e.ToSl ring (), "Encryption Error"fe- MessaileBexButtoris 0f(. MessageBoxIcon. Stop j ; " return false; ; • i: I Процесс дешифрования выполняет противоположные действия. Рассмотрим код, находящийся под кнопкой Decrypt. Сначала получаем имя первоначального файла из выбранного имени, удаляя расширение .епс. Затем вызываем метод decryptData(); передавая ему имя источника, назначения и секретный ключ. private void button2_Click(object sender, System.EventArgs e) string decFileName - decFile.Text. Replace C4enc% ’‘”); if (decryptData(decFile.Text, decFilerlame, txtKey.Text) » true) MessageBox.Show(this, ''Done!”, Decryption Status”, MessageBoxButtons.OK, MessageBoxIcon.Information); else -• > I MessageBox.Show(this, “The decryption process failed!”, “Fatal Error”, MessageBoxButtons.OK, MessageBoxIcon Stop); Метод decryptData( )очень похож на encryptData(). Сначала создаем объект типа DESCryptoServiceProvider и назначаем ему ключ. Затем генерируем новый VI и созда- ем новый объект DES-дешифрования, вызывая метод CreateDecryptor(). Далее счи- тываем файл-источник в CrytoStream и преобразуем содержимое в новый файл. Наконец закрываем потоковые объекты и возвращаем значение true: // Метод £ecryptData дешифрует заданный файл через алгоритм DES public bool oecryptData(string soqfceFiler string destinationFile, r - О . strings ryptokey): 'try . • ....... о .. ' .... .. .. .
368 Глава 10 // Создаем объект провайдера сервиса DES и назначаем ключ и ректор DESCryptoServiceProvider DESPrqvider = new DESCryptoServiceProviderO; DES Pro vide г. Key - ASCIIEncoding. AStHGetBytes( cryptoKey); DESProvider.GenerateIV(j; ’«* и 1г“ - ; n '• FileStream DecryptedEile = new FileStream(sourceFile, FileMode.Open, S"'T' %? -v . v< ' . / FileAccess.,Read), W ICryptoTransform desDecrypt - DESProvider.CreateDecryptorO; x • J, ' ‘ । 4 r' - \ J* ’ i.l, ;• , <«1 . , # - ? - CryptoStream cryptostreamDecr - new CryptoStream(DecryptedFile, desDecrypt,CryptoStreamMode.Read); Streamwriter DecryptedOutput = new St reamWrite'"(destinationFile) ; OecryptedOutput.write(new StreamReader(cryptostreamDecr).ReadToEhd()); DecryptedOutput Flush(); DecryptedOutput. Close(); return true; } '' . i catch (Exception e) MessageBox,Show(this, e.ToStringO, '’Decryption Error", return false; V : 3- a MessageBoxButtons.OK, MessageBoxIcon.Stop); Использование других симметричных алгоритмов Поскольку все симметричные алгоритмы порождены от класса SymmetricAl- gorithm, очень легко реализовать процесс шифрования-дешифрования на основе рассмотренного ранее кода. Например, если вы хотите использовать алгоритм RC2, Triple-DES или Rijndael, то достаточно лишь заменить в методах encryptDataO и decryptData() следующее объявление соответствующими объявлениями, показанны- ми далее: □ DES DESCryptoServiceProvider DESProvider = new DESCryptoServiceProviderO; □ RC2 RC2CryptoServiceProvider RC2Provider = new RC2CryptoServiceProvider(); O Triple-DES TripleDESCryptoServicePravider tDESProvider = new ' TripleDESCryptoServiceProviderO; w □ Rijndael RijndaelManaged RijndaelProvider = new RijndaelManagedO; Если вы внесете это изменение, то ваше приложение шифрования-дешифрова- ния файлов будет прекрасно работать. Успех процесса симметричного шифрования и дешифрования основан на зна- чении 1(люча. Если не передать алгоритму ключ допустимой длины, будет по- рождено исключение CryptographicException. Размеры ключа, поддерживаемые
Криптография B.NET 369 алгоритмом, можно получить, обратившись к свойству LegalKeySizes. Это свойство возвращает массив объектов KeySize. В классе KeySize есть три целочисленных от- крытых свойства—MaxSize, MinSize и Ski pSize. В свойствах MaxSize иMinSize задают- ся соответственно максимальный размер ключа (в битах) и минимальный размер ключа (в битах). Свойство Ski pSize возвращает интервал между допустимыми значе- ниями ключа в бцтах. В следующей таблице перечислены размеры ключа, поддерживаемые в основ- ных алгоритмах: Алгоритм Размер ключа DES 64 бита или 8 байтов RC2 128 битов или 16 байтов Triple-DES 192 бита или 24 байта Rijndael 256 битов или 32 байта Ключ составляет важную сторону шифрования. Чем длиннее ключ, тем лучше шифрование. А значит, вероятность того, что хакеру удастся атакой “в лоб" расшиф- ровать данные, уменьшается. Однако нельзя забывать еще об одном ограничении — чем длиннее ключ, тем больше времени требуется на процессы шифрования и де- шифрования. Асимметричные преобразования в .NET Как обсуждалось ранее, асимметричные алгоритмы базируются на концепции открытого и закрытого ключей (PKI). Пространство имен System.Security.Cryp- tography поддерживает два асимметричных алгоритма: RSA и DSA. Класс AsymmetricAlgorithm Обаалгорйтма, RSAh DSA, наследуют базовому классу AsymmetricAlgorithm. Класс Asymmet ricAlgorithm предъявляет несколько общих методов и свойств, которые мож- но использовать для всех алгоритмов хеширования: Член класса Описание KeySize и KeySizeValue Указывают размер модулей ключа в битах. KeySize — это открытое свойство, KeySizeValue — защищенное поле, оба свойства возвращают целое значение, представляющее размер ключа в битах. LegalKeySizes и LegalKeySizesValue Указывают допустимый размер ключа для текущего асимметричного алгоритма. LegalKeySizes — это открытое свойство, а LegalKeySizesValue — защищенное поле, оба свойства возвращают массив KeySizes. KeyExchangeAlgo гit hm Задает алгоритм обмена ключом, используемый при взаимодействии между сторонами, и способ обмена открытым и закрытым ключами. Это открытое свойство возвращает строку, представляющую имя алгоритма обмена ключом.
370 ГлавсИО продолжение таблицы SignatureAlgorithm Задает алгоритм, используемый для подписи текущего объекта. Это открытое свойство возвращает строку, представляющую имя алгоритма подписи. FromXmlStringO Восстанавливает асимметричный объект из XML-файла. Это открытый метод, принимающий на входе строку. ToXmlStringO Возвращает XML-представление объекта текущего алгоритма. Это открытый метод, возвращающий строку. Уже обсуждалось, что многие криптографические алгоритмы реализованы по- верх библиотеки Crypto API. Среда .NET Framework создает на базе библиотеки Crypto API наборы управляемых классов, называемых провайдерами криптографи- ческих служб (Cryptographic Service Providers, или CSP). Класс CspParameters ис- пользуется для отправки значений неуправляемому Crypto API и для получения значений от этого API. Провайдеры криптографических сяуэ еб представляют собой подключаемые программы (plug-ins) для Crypto API. Эти программы действуют, как механизмы шифрования, выполняющие процесс шифрования-дешифрования. Операция CSP базируется на значении перечисления CspProViderFlags. В этом перечислении поддерживаются два значения: UseDefaultKeyContainer и UseMachine- KeyStore. Если задана опция UseDefaultKeyContainer, информация о ключе считыва- ется из контейнера ключа по умолчанию. Если указана опций UseMachineKeyStore, информация о ключе считывается из контейнера ключа компьютера. Для хранения пар открытых и закрытых ключей провайдеры поддерживают базу данных. Некоторые CSP поддерживают контейнеры для ключей в реестре, некоторые-в других местах, например, на смарт-картах или в шифрованных, скрытых файлах. Использование алгоритма RSA Алгоритм RSA реализован в классе RSACryptoServiceProvideг, который наследует классу RSA, а тот, в свою очередь, порожден от класса Asymmet ricAlgorithm. Алгоритм RSA позволяет шифровать, дешифровать данные, подписывать их цифровой под- писью и проверять подпись. Все эти возможности одну за другой рассмотрим в этом разделе. Начнем с простого подхода шифрования-дешифрования. Поскольку RSA отно- сится к асимметричным алгоритмам, он работает медленнее своих симметричных аналогов. Следовательно, алгоритм RSA лучше всего применять для шифрования небольших сообщений. Соответственно, построим Windows-приложение, которое будет шифровать и дешифровать введенное пользователем сообщение с использо- ванием технологии PKI. Всякий раз, когда вы создаете новый, используемый по умолчанию экземпляр конструктора класса RSACryptoServiceProvider, автоматически создается готовый к использованию новый набор информации об открытых и закрытых ключах. Кроме того, мы можем хранить PKI-значения в XML-файлах. Как раз это и демонстрирует- ся в первом примере. Сначала создадим пользовательский интерфейс, который по- казан на следующем снимке экрана:
Криптография в .NET 371 Этот интерфейс дает возможность видеть открытый текст и закодированный шифротекст. Код также будет показывать открытый и закрытый ключи, используе- мые в этом процессе, и позволит сохранять открытый и закрытый ключи в отдельных XML-файлах. Поскольку мы собираемся использовать автоматически генерируемые в RSA от- крытые и закрытые ключи, объявим статический объект уровня класса. static RSACryptoServiceProvider rsaProvider; Вот код для кнопки Encrypt: if (txtClearTexr. Text Trim/) 1= “1’) { . *-T // Инициализируем RSA-провайдер (CSP) rsaProvider = new RSACryptoSeryiceProviderO; UTF8Encoding utfB ^new (JTF8Encoding(); byteEJ cleartext = utf8.GetBytes(txtClearText.Text.TrimO)); s Z/ Шифруем полученные данные txtCipherTexLText - Convert.ToBase64String(rsaProvider.Encrypt(clearText, ", . .false)) : ' 4 : k J . * - W-vA; ‘>4 // Показываем закрытый ключ txtPrvKey.Text » rsaProvider.ToXmlString(true); .// Показываем открытый ключ * txtPubKey.Text = rsaProvider. ToXmlString(false); Сначала создаем новый объект типа RSACryptoServiceProvider. Затем с помощью класса UTF8Encoding преобразуем введенное пользователем сообщение в массив бай- тов. Потом вызываем метод EncryptO класса RSACryptoServiceProvider и передаем ему входной массив байтов и второй параметр со значением false. Второй параметр метода Encryp t () имеет отношение к режиму выполнения. Если вы работаете в Windows 2000 ОС с SP2 или следующих версий, то в этом параметре можете установить значение true и тогда будет использоваться метод заполнения ОАЕР. Когда устанавливаем значение false, будет использоваться PKCS версии 1.5.
372 Глава 10 Метод Encrypt () возвращает зашифрованный массив байтов. Используем метод ToBase64String() класса Convert, чтобы преобразовать массив байтов в строку, ото- . в текстовом поле. Мы также отображаем открытый и закрытый ключи, сгенерированные алгоритмом RSA, в двух текстовых полях с использованием мето- да ToXmlString() класса RSACryptoServiceProvider. Этот метод принимает на входе бу- лево значение, и если оно равно false, то в ХМ-Ьстроке генерируется значение открытого ключа, если же оно равно true, — генерируется закрытый ключ. Далее приводится код для кнопки Decrypt. Этот метод только вызывает Decrypt () объекта RSAC ryptoSe rviceP rovide г: prTvate~void ЬтпОесгур1_С11ск(оЬ, ect sender * System EventArgs e) { * ‘ " if (txtCipherfext.Text.TrimO != ’°’) // Преобразуем входную строку в массив байто byte[] bCipner1ext,= й Convert.FromBase64String(txtCipberlext .Text.Trim()).; // Выполняем декодирование данных и преобразование в строку Г string strvalue - 1 -v = •- ASCIIEncoding.ASCII.GetString(rsaProvider.Decrypt( bCipherText, falsa)); /7 Сл обращаем дешифрованную строку в MessageBox MessageBox.Showfthis, strValue, “Decrypted value', MessageroxButtons.OK, 1-lessageBoxIcon.Information); } } Теперь рассмотрим сохранение открытого и закрытого ключа в XML-файле. Мы отображаем общий диалог SaveFile, чтобы получить от пользователя нужное имя файла. Затем загружаем XML-данные в объект XmlDocument и, чтобы сохранить данные на диске, вызываем метод Save(): private void btnSave_ClicR(object sender, System EventArgs e) { SaveFileDialog saveFileDialogl = new SaveFileDialogl). 4 saveFileDialogl.Filter = ‘’XML files (<xitiifc|* xmljAll files (*i*)|*.*"; savtEileDialogl rtlterlndex == 2; 9 1 •' saveFileDialogl.RestoreDirecwry- true; 4^ ’</ n*’ • iffsaveFileOialogl.SnowDialogO » DialogRasult.OK) ff Записываем содержание XML-файла XmlDocument xmlDoc = new XmlDocument^); xmlDoc.LoadXml(this. txtPubKey1 Text); л // Сохраняем документ в файле xmlDoc.SavetsaveFileDlalogl.OpenFile()); ; у 4- >>Л : ’ 1 • ’ ' - Далее это приложение показано в действии:
Криптография в .NET 373 Как можно видеть, на экране показаны: открытый текст, шифротекст и инфор- мация открытого и закрытого ключей. Загрузка открытых и закрытых ключей В предыдущем примере было показано, как можно шифровать и дешифровать данные с использованием автоматически сгенерированных открытого и закрытого ключей. Мы также узнали, как сохранять ключи в XML-файле. Однако если надо повторно использовать созданные ранее или сохраненные ключи, этого можно достичь, проинициализировав класс заполненным объектом CspParameters. Рассмот- рим это на примере. Создадим такой же пользовательский интерфейс, как в преды- дущем приложении, но в этот раз будем загружать открытый и закрытый ключи из XML-файлов: Вот какой код мы написали для загрузки ключа из XML. Сначала отображаем диалоговое окно OpenFile и получаем в нем от пользователя имя файла. Затем загружаем файл в объект XmlTextReader и отображаем информацию ключа в соответствующем текстовом поле:
374 Глава 10 private void btnLoadPub_Click(object sender, System.EventArgs e) { ' • 7/ Отображаем диалог открытия файла openFileDialog!.Title * “Select the Public Key file"; openFileDialog!, Filter = “XML Files (*.xml)|*.xml”; if(openFileDialpg!,ShowDialog() == DialogResult.OK) string fileName = openFileDialogl.FileName; btnEncrypt!.Enabled = true; // Загружаем документ XmlTextReader xmifieader = new XmlTextfleader(fileName)-; xmlReader.WhitespaceHandling = WhitespaceHandling.None; xmlRe&der..ReqdQ; // Назначаем текстовому полю открытый ключ txtPubKeyl.Text - xmlReader ReadOuterXmlO; Теперь рассмотрим код для кнопки Encrypt. Во-первых, создаем объект класса CspParameters и устанавливаем флаг использования хранилища машины для поиска ключей PKI. Затем даем контейнеру ключа имя WroxRSAStore. private void btnEncrypt1_Click(object sender, System.EventArgs e) if (txtClearTextl.Text.TrimO !- "”) { try { ' CspParameters cspParam = new CspParameters(); cspParam.Flags = CspProviderFlags.UseMachineKeyStore; cspParam. KeyContainerName = “WroxRSAStore’*; л = cspParam.ProviderName = “MS Strong Cryptographic Provider"; fl Константа CryptoAPI -> PROV_RSA_FULL = 1 > // Этот тип провайдера поддерживает как цифровую подпись, // так и шифрование данных и считается универсальным £ // Алгоритм RSA для открытого ключа используется // для всех операций с открытыми ключами. cspParam.ProviderType = 1; Закончив с инициализацией параметров для CSP, создаем новый объект типа RSACryptoServiceProvider, использующий объект CspParameters. Затем назначаем от- крытый ключ, вызывая метод FromXmlString(). После этого выполняем привычный для нас процесс преобразования строки в массив байтов, передачи массива байтов в метод Encrypt() и преобразования массива байтов обратно в строку: // Инициализация CSP RSA RSACryptoServiceProvider rsaProvider! = new RSAC ryptoSe rviceP royide r(cspPa ram); // Загружаем открытый ключ rsaProvider!.FromXmlString (txtPubKeyl.Text) UTF8Encoding utf8 = new UTF8Encoding(); byte[J clearText - utf8 CetBytes(txtClearText!.Text); // Преобразуем зашифрованный текст в base64 txtCipherTextl.Text '= Convert.ToBase64String( rsaProvider!. Eric rypt(c lea rText, false)); } . 1 ; •. . ; .. .
Криптография в .NET 375 т Sb catc (Exception e) if ’ i :4 • , . a*. ,AM* . . • MessageBox.ShowCthis, e ToStringQ-); } J : Метод дешифрования снова чрезвычайно прост—создаем объект CSP RSA и на- значаем ему закрытый ключ. Затем дешифруем сообщение, используя метод Decrypt(): privatevoid btnDecrypt1_Click(object sends г, System.EventArgs e) { if (txtCleariext1.Text Trim() ! = "”) 4 л JJ Инициализируем CSP RSA RSACry^toServiceProvider rsaProviderl = new RSACryptoServiueProviderO; // Загружаем закрытый ключ rsaPrpvide ri. FromXmlString(this.txtPriKey!.Text), // Шифруем полученные данные ч, . ₽ MessageBox.Show(this, ASCIIEncoojng, ASCII.Getstring( t rsaProviderl. Dec rypt (Convert. FrpmBase64StdLrig( Jkg, 4, txrCipnerText1.TPxt.Trim())1 false». “Cecrynted value’*, <-• MessaaeBoxButtons.OK, MessageBoxIcori Information); T- ) • r'' -f f - Так выглядит приложение в работе: Об одном ограничении алгоритма RSA следует знать - метод Encrypt () может шифровать до 16 байтов, только если установлен пакет High Encryption. Иначе он может шифровать лишь 5 байтов. Чтение сертификата Х509 Сертификат напоминает “ваучер”, содержащий информацию о лице, которое владеет ваучером, в том числе, кто выдал сертификат, его открытые ключи и ин- формацию о сроке действия. Сертификаты подписываются уполномоченными организациями (certifying authority, или СА), такими, как VeriSign или Thawte. Сер-
376 Глава 10 вер сертификатов Microsoft также позволяет создавать “самоподписанные” серти- фикаты. И хотя весь остальной мир не может им доверять, поскольку они не выданы хорошо известной СА, однако они очень полезны в сценарии интрасети. Серверные сертификаты используются для установления достоверности серве- ра, а клиентские сертификаты — для идентификации клиента данного сервера. СА выдают клиентские и серверные сертификаты после удостоверения их подлин- ности. Например, когда клиент запрашивает Web-pecypc, он может также отпра- вить вместе с запросом клиентский сертификат. Тогда сервер может определить, кто этот клиент, и, соответственно, разрешить или запретить ему доступ. Компания Thawte предоставляет клиентский сертификат бесплатно, a VeriSign взимает за него плату. Сертификаты клиентов обычно установлены на таких Web-клиентах, как браузе- ры или клиенты электронной почты. Можно просмотреть все установленные сер- тификаты клиентов в IE, щелкнув по пункту меню Tools I Internet Options... и выбрав в диалоговом окне закладку Content. Щелкните по кнопке Certificates... Как вы мо- жете видеть, в моем IE 6 установлены два клиентских сертификата: Пространство имен Cryptography также содержит порожденное пространство имен X509Certif icates. В нем содержатся только три класса, используемых для пред- ставления сертификатов Authenticode Х509 v.3 и управления ими. Класс X509Certi- f icate предъявляет статичные методы CreateFromCertFile() и CreateFromSignedFile(), предназначенные для создания экземпляра сертификата. Мегод CreateFromCertFileO считывает содержание сертификата Х509 из файла сертификата, а метод CreateFromSignedFile() — из файла с цифровой подписью. Про- читаем содержание клиентского сертификатаХ509 методом CreateFromCertFile(): ' * pate btnView.cnckCobjert semer System,fventArgs e)
Криптография в .NET 377 / Считываем клиентский сертификат из тайла // в переменную-объект типа X509Certificate X509Certificate clientCert s „ X509Certifica ie.CreateFromCertFile("C:\\test.cer’); StringBuilder sb = new StringBuilderO; so.Append(11 Issuer Name: " + clientCert.GetlssuerNameO + “\n”); sti.Append("Public Key String. ” + clientCert.GetPublicKeyString() + "\n”); sb.Append(“Key Algorithm: ” + clientCert.GetKeyAlgorithm() ToStringO + "\n”): sb.Append("Serial Number: " + clientCert. GetSerialNumberStnng() + *‘\iT); sb.Append^"Effective Date ” + clientCert.GetEffectiveDateStringO ToStringO + “\n”); sb.Append("Expiration Date: ” + clientCert. GetExpirationDateStringO. ToStringO + “\n”); MessageBox.Show(this. sb.ToSt ring()); Мы объявили объект типа X509Certi float е и использовали метод С reate FromCert- File() для считывания содержания сертификата в объект. Затем прочитали основ- ные свойства сертификата и отобразили их в окне сообщения: Криптография и сетевое программирование До сих пор в этой главе мы изучали только криптографию — теперь наступило время применить некоторые из этих приемов в сетевом программировании. Пом- ните простую утилиту интерактивного форума, которую мы написали в главе 6? Очень несложное приложение на UDP. Оно принимает номера локального и уда- ленного портов, IP-адрес и, используя их, рассылает введенную информацию. Вот код приложения до подключения криптографических алгоритмов. Пока- занный далее метод Send() получает дейтаграмму, преобразует ее в массив байтов и отправляет данные, используя метод Send() класса UdpClient. Метод ReceiverO ра- ботает так же. private static void Send(string datagram) { // Создаем UdpClient UdpClient sender = new UdpClientO; 11 Создаем IPEndPoint с данными об удаленном хосте IPEndPoint endPoint = new IPEndPoint(remotelPAddress, remotePort); try // Преобразуем данные в массив байтов
378 " ~ ___ r. ГлаваЮ byte[] bytes = Encoding.ASCII.GetBytes(datagram); // Отправляем данные sender.Send(bytes, bytes.Length, endPoint); , } catch (Exception e) { Console.WriteLine(e.ToSt ring()); } finally { 1 // Закрываем соединение sender. CloseO; } public static void ReceiverQ { // Создаем UdpCIient для считывания входящих данных UdpCIient receivingUdpClient = new UdpClient(localPort); // IPEndPoint с данными об удаленном хосте IPEndPoint RemotelpEndPoint = pull; try { Console.WriteLine( “-------*******Ready for chat!!!*******--------”); while(true) { // Ждем дейтаграмму byte[] receiveBytes = receivingUdpClient Receive( ref RemotelpEndPoint); . // Преобразуем и отображаем данные string returnData = Encoding ASCII.GetString(receiveBytes); Console. WriteLine(“~" + returnData.ToStringO); } } catch (Exception e) Console. WriteLine(e. ToStringO); } ) Для отправки защищенной информации от одного клиента форума другому бу- дем пользоваться симметричным алгоритмом Rijndael. Этим симметричным алго- ритмом лучше пользоваться при передаче объемных данных, и, главным образом, поэтому я выбрал именно его. Чтобы позаботиться о защите процесса обмена, мы собираемся добавить в этот класс методы encryptData() и decryptData(). При отправ- ке информации удаленному сокету вызываем метод encryptData(), шифруем данные и передаем зашифрованный массив байтов другой Стороне. Получив там шифрован- ный массив байтов, вызываем метод dec ryptData(), который дешифрует этот массив. Как можно видеть, наша конструкция очень проста. Для поддержки симметрич- ных алгоритмов добавляем на уровне класса два закрытых члена, в которых будем хранить общий ключ и вектор. private static IPAddress remotelPAddress; private static int remotePort;
Криптография: в .NET 379 private static int localPort;. private static UTFSEncoding Utf8Encotf; / private static string CryptoKey «• “!1''6охШ}12К‘у$”; private static string CryptoVj - ‘"~x70q{6+q1@#VI$”; Метод encryptData() принимает на входе строку и возвращает массив байтов. static byteQ encrvptData( string theDataGram) { . S i byte£] bCipherText <= null; try { Создаем новый объект типа Rij ndaelManaged, назначаем общий ключ и вектор. За- тем, используя метод CreateEncryptor(),создаем объект ICryptoTransform. // Создаем объект провайдера сервиса Rijndael и. назначаем ему // ключ и вектор ••• RijndaelManaged RijndaelProvider = new RijndaelMana'jedO ; RijndaelProvider.Key * UtfSEncod GetBytes(CryptoKey); RijndaelProvider.IV = UtfSencod. GetBytes(CryptoVI); ICryptoTransf©rm RijndaelEncrypt = RijndaelProvider.CreateEncryptor(); Теперь с помощью класса UTFSEncoding преобразуем дейтаграмму в массив бай- тов. Объявляем объект MemoryStream и используем объект CryptoStream для выполне- ния криптографического преобразования (см. главу 2). // Преобразуем строку в массив байтов byte[] bClearText = Utf8Encod.GetBytes(theDataGram); MemoryStream Mstm = new MemoryStreamO ; // Создаем CryptoStream, преобразующий поток, используя Шифрование CryptoStream Cstm - new CryptoStream(Mstm, RijndaelEncrypt, CryptoSt"earnMode W-ite); / // Записываем зашифрованное содержание в MemoryStream Cstm.Write(bClearText, 0, bClearText.Lengthj; Ctms. FlushFinalBlockO ; Снова создаем массив байтов из MemoryStream и возвращаем его вызывающему коду. * // Получаем выход bCipherText = Mstm. ToAr ray О; // Закрываем обработчики потоков Cstm. CloseO; Mstm. CloseO; } catch (Ekceotion e) { Console.WriteLine(e.ToString () ) ; return bCipherText; Метод decryptDataO, выполняющий дешифрование, аналогичен методу en- cryptData(). Он получает на входе массив байтов и возвращает на выходе строку. static string decryp€Data(byte[] bCipherText) * { ' ы ' " ' string sEncoded ~
380 Глава 10 try • { ; // Создаем объект провайдера сервиса RijndaelManaged и назначаем ему // ключ и вектор . ... RijndaelManaged RijndaelPiovider = new RijndaelManagedO; RijndaelProvider.Key - Utf8Encod GetBytes(strCryptoK5y); RijndaelProvider.IV = Utf8Encod GetBytes(strCryptoVI); ICryptoTransform RljndaelDecrypt= RijndaelProvider CreaceDecryptorO; // Создаем MpmcrryStreaw, передавая входные параметры Memo.yStream Mstm - new MemoryStream(bCipherText, 0., / bCipherText.Length); // Создаем CrvptoStTeam преобразующий строку, используя дешифрование CryptoStream Cstm * new CryptoStream(MsCm, RijndaelDecrypt, C ryptoSireamMode.Read); /7 Считываем результат из- Crypm Stream StreamReader Sr » new StiaamReader(CsLm) sEncoded - Sr.ReadToEndt); * Sr.Closet); Cstm.Closet); - Mstm.Closet)* 'S' } catch (Exception e) { > - Console. Write! l ne (e ToString f):); } ! eturn sEricoded; - } ...... Теперь в нужных местах приложения интерактивного форума установим вы- зовы методов encryptDataO и decryptData(). Метод Send() вызывает метод en- crypt Data(), передавая строку, введенную пользователем, и отправляет результат через метод Send() класса UdpClient. Метод Receiver() передает.полученный массив байтов методу decryptData() и отображает дешифрованное сообщение. private static void Send(string datagram) < try { // Преобразуем строку в массив байтов //byte[] bClearText = Utf8Encod.GetBytes(datagram); 7/ Шифрован в полученных данных byfel] oytes e,icr^ptData(datagram); ,4, // Отправляем данные sender.Send(bytes, bytes.Length, endPoint); } } public static void Receiver() { while(true) {
381 Криптография в .NET - ... , v„ - ♦ 11 Ждем дейтаграммы byte[] receiveBytes = receivingUdpClient.Receive(ref RemotelpEndPoint); // Дешифруем входящий массив байтов string returnData - decryptData(receiveBytes); Console. WriteLine(“-" + returnData.ToStringO); To же самое можно сделать для другого симметричного алгоритма; выберем, на- пример, RSA. Просто перепишем методы encryptDataO и decryptDataO, используя алгоритм RSA. Остальная часть реализации не претерпит никаких изменений. Прежде чем писать методы encryptData() и decryptDataO, надо сохранить откры- тый ключ и закрытый ключ в закрытых переменных уровня класса. private static IPAddress remotelPAddress; private static int remotePort; private static int localPort; private static UTF8Encoding Utf8Encod; private static string PubKey = w<RSAKeyValue><Modulus>sttDl3xug/BqMk13d6G5vWekmyul/d3pz/Lpvk2Q1G NBSriatLxCRJSu0Aie8g/yby624K85qJLwMMzwCru7b+kNTA2dYaK4Nk+FkZM LCVmomiW1zns2KsT1aF9hwr32Nyje30eJDlHqBtc0pCGbo+kJ+JC88BM1J9Akd oAa+SE=</Modulus><Exponent>AQAB</Exponent></RSAKeyValue>"; private static string PriKey = “<RSAKeyValue><Modulus>sttDL3xug/BqMkl3d6G5vWekmyul/d3pz/Lpvk2Q1G NBSriatLxCRJSu0Aie8g/yby624K85qJLwMMzwCru7b+kNTA2dYaK4Nk+FkZM LCVmbmiWlzns2KsTlaF9hwr32Nyje30uJDlHqBtc0pCGbo+kJ+Jt;88B>f1J9Akd v oAa+$E:’</ModulusxExponent>AQAB</Exponent>3BolsxTvnh8Xtg/02fTGt r/k80XU0iEfKwAKzWje36v8zkTfIc4EzdZbRskJywq1NMo9UlEHM3DUv+Ya/K GPzQ==x/PxQ>0AcCph/CdQeB2/M+q3BSlZirtir9Chw9zaHk1x8MBCHdRB9c26 VcS0AmKW+G4VzjWJjI6cK8j/GQjhnRn7UbBypQ==</Q>bikCjwD+gPRs6 KmJ0gCp6F0Y4V0WYFWthNcLkQ1Y5zfsWsyrpP649tC/dGkwZpggY6CJGwcm BIAHa1he22yJTO==</DP><DQ>Uzva1Xkzpvuf+89xrcq9YQArwYDqmKGPLDy 0cC2cxq6czarI+XRAyguEeFYjp2RIatLMrcA4QV4KV3+DzQWaeQ==</DQXlnv erseQ>eriVG9Kp3CQ/J9PpfMlemC7tPls6m//LyhKD7J5zLGIzz+71C5QjVi2dRwtv jGJaexOTi+TRIv2fT/thWmsCDQ==</lnverseQ>sjfRZ470tIuf1gXY8AznfnL C05eXrDIuo/YBsY2qredFDQaLqWIZiiq4ur7kWoFHakAbHCGeC3p2+bmLyrYr2n m80gj0c1NUneE8ASoKWfnbcWxW3770eogj16frPUoAgwUlgFURdTxozgNLTb VtNItrc3Doa5eJ+U7pRSz2edE=</DX/RSAKeyValue>"; Вот как выглядят методы encryptDataO и decryptDataO. В методе encryptDataO со- здаем новый объект CspParameters и используем его для создания нового объекта ал- горитма RSA. Затем назначаем открытый ключ из закрытого члена класса. Наконец вызываем метод Encrypt() объектаRSA. static byte[] encryptData(string strDataGram) CspParameters theCspParam » new CspParametersO; theCspParam.Flags « CspProviderFlags.UseMachineKeyStore; theCspParam.KeyContainerName = "WroxRSAStore”; ... V 7 Инициализируем CSP RSA t : 7 RSACryptoServiceProvider theRSAProvider « new . 7- - RSACryptoServiceProvider(objCspParam);
382 Глава 10 If Загружаем открытый ключ theRSAProvide r.FromXmlSt ring(PubKey), // Преобразуем строку в массив байтов byte[] bClearText = Utf8Encod.GetBytes(str0ataGram); r ff Шифруем полученные данные byte[j bytes - theRSAProvider.fncrypt(bClearText, false); theRSAProvider. ClearO; return bytes; } Метод decryptData() делает почти то же самое. Он создает новый объект RSA и назначает закрытый ключ из члена класса. Затем он вызывает метод Dec rypt () и от- правляет строку обратно вызывающему коду. static string decryptData(byte[] bCipherText) ' { // Инициализация CSP RSA RSACryptoServiceProvider theRSAProvider = new RSACr.yptoServiceProvider():; ,// Загружаем закрытый ключ theRSAP rovide r.FromXmlSt ring(PriKey); //• Дешифруем полученные данные string strRtnOata - Utf8Encod.GetString(theRSAProvider.Decrypt (bCipherText,false) ) ; theRSAProvider Clear() ; return strRtnOata; > Данную реализацию методов encryptData() и decryptDataO можно включить в предыдущий пример, и код будет отлично работать. В этом примере нужно только побеспокоиться об ограничениях размера, предъявляемых методом Епс rypt (). Эти ограничения уже обсуждались выше. Итоги В этой главе мы обсудили массу вопросов, начав с краткой истории криптогра- фии. Тщательно рассмотрели причины, заставляющие нас использовать крипто- графию, и привели пример, показывающий, что передаваемая информация видна всем. Затем мы представили разные типы криптографических алгоритмов: симмет- ричные и асимметричные алгоритмы, алгоритмы хеширования, а потом перешли к детальному рассмотрению работы каждого из этих типов. Далее рассказали о та- ких основных концепциях криптографии, как блочные и поточные шифры, запол- нение и режим. После общего введения в криптографию мы обратились к специфике использо- вания криптография в .NET. Изучили пространство имен System.Security.Cryp- tography, в том числе иерархию криптографических классов, после чего перешли к разбору примеров, демонстрирующих использование в .NET хеширования, сим- метричных и асимметричных алгоритмов. Затем подробно рассмотрели RSA-шиф- рование и затронули доступ из .NET к сертификатам Х509. Наконец познакомились с некоторыми примерами использования криптографии в сетевом программирова- нии с применением алгоритмов Rijndael и RSA.
ГЛАВА 11 Протоколы аутентификации Аутентификация стала основным вопросом для любого разработчика приложе- ний, рассчитывающего, что его код будет выполняться в сети или в Интернете. Га- рантировать, что пользователи являются именно теми, за кого себя выдают, и устанавливать по требованию подлинность машины обязан модуль безопасности приложения. Разработчики Windows-приложений, кроме дополнительных мер бе- зопасности, определяемых приложениями, должны выполнять специфичные для среды Windows процедуры безопасности, реализованные компанией Microsoft. После слабых реализаций средств безопасности в ранних версиях Windows NT и в семействе операционных систем Windows 9Х, в последующих системах — Windows NT 4.0 и Windows 2000 — аутентификации уделяется особое внимание. При этом вместо того, чтобы придумывать новую систему аутентификации с чистого листа, Microsoft исследовала существующие методы аутентификации и приняла их для использования в Windows 2000, особенно систему Kerberos. В этой главе рассказано, какие протоколы аутентификации выбраны для схем сетевого обмена Microsoft, как они работают и как применяются в разных версиях Windows. Протоколы аутентификации Рассмотрим основные протоколы аутентификации, используемые в Windows и .NET. К ним относятся: □ LANMAN (LAN Manager компании Microsoft) О NTLM (Windows NT LAN Manager) □ Kerberos Однако, прежде надо кратко упомянуть о протоколе защищенных сокетов (Secu- re Sockets Layer, или SSL), который используется для отправки шифрованных дан- ных. Протокол SSL прошел после введения (компанией Netscape) несколько
384 Глава 11 итераций и был включен в протокол безопасности транспортного уровня (Trans- port Layer Security, или TLS), разработанный IETF. Поскольку SSL и TLS используют- ся для отправки информации аутентификации по Интернету, нужно иметь общее представление об их работе. SSL — это протокол, располагающийся между прикладным уровнем и лежащим внизу уровнем TCP. Уровень SSL должен обеспечивать шифрование, аутентифика- цию и информацию, подтверждающую целостность данных, используя разные дан- ные для каждой из этих задач. Поскольку SSL старается скрыть эти сложности от пользователя (и разработчика), реально применить SSL исключительно просто. Если в вашем компьютере подключено программное обеспечение шифрования, вы можете заменить в URL http на https, и в результате браузер будет вызывать компо- ненты SSL. Когда используется https, t целевым URL устанавливается зашифрован- ное соединение. NTLM Аутентификация NT LAN Manager (NTLM) в Microsoft Windows—это система ау- тентификации с разовым паролем, вызовом и откликом (challenge-response). Этот тип системы аутентификации возник много лет назад и использовался в самых раз- ных целях. В простейшей своей форме система аутентификации с вызовом и откли- ком отображает вызов (также называемый попсе), представляющий собой число или строку. Отклик создается на основании вызова, и если он принят, то на этом аутентификация и завершается. Обычно это разовая аутентификация, которая вы- полняется в начале сеанса и действует до его конца. В первых системах аутентификации с вызовом и откликом использовался руч- ной поиск: компьютер отображал вызов, а пользователь должен был найти отклик в книге или программном инструменте (называвшемся парольным жетоном) и от- править его компьютеру. Этот процесс очень быстро автоматизировали, например, при попытках пользователя обратиться к серверному ресурсу. Сервер генерировал и отправлял вызов, который обрабатывался программами клиента для выработки отклика на основании их алгоритмов, а сервер подтверждал правильность отклика, и клиент признавался идентифицированным. Системы с вызовом и откликом обладают несколькими основными преимущест- вами. Поскольку вызов может генерироваться случайным образом каждый раз, ког- да это необходимо, то пары вызов-отклик не должны храниться в файле или алгоритме, где они могут подвергнуться атакам Со стороны хакеров пли обратному проектированию (reverse-engineering attackts). В этой системе повторное использо- вание вызова и соответствующего пароля крайне маловероятно, что не позволяет хакера м воспользоваться повторяющимися кодами. Кроме того, не требуется под- держивать синхронизацию между клиентом и сервером. Когда бы клиент ни вступал в контакт с сервером, генерировался случайный вызов и вычислялся правильный отклик. Главный недостаток системы с вызовом и откликом состоит в том, что кли- ент должен по запросу генерировать надлежащий отклик, что означает или вовлече- ние пользователя в операцию поиска, или использование на клиенте некоторого алгоритма. Чтобы помешать нежелательным атакам, клиентский алгоритм обычно является односторонней системой, не позволяющей генерировать вызовы, а толь- ко порождающей отклики. Во многих современных системах аутентификации с вызовом и откликом со- блюдаются стандарты, принятые Ассоциацией американских банков (American Bankers Association) (стандарт Х9.9) и правительством США (FIPS 113). В обоих ва- риантах используется алгоритм DES как односторонняя хеш-функцйя. Когда сервер отправляет вызов, он шифруется с использованием DES и секретного ключа, встро-
Протоколы аутентификации . _ 333 енного в аппаратное и программное обеспечение. Затем внутренние алгоритмы со- здают отклик-пароль, допустимый лишь на один раз. Публикация этих стандартов не только помогает разработчикам программного обеспечения решить, достаточно ли безопасны для использования системы аутентификации с вызовом и откликом, но и дает возможность облегчить их реализацию в соответствии со стандартами. Когда компания Microsoft разрабатывала свой продукт LAN Manager, ей была нужна система аутентификации, которая не только позволила бы операционной системе Windows NT (в то время новой системе), но также более старым PC, рабо- тающим под управлением DOS и Windows, безопасно участвовать в транзакциях клиента с сервером. По этой причине в LAN Manager использовалась система аутен- тификации с вызовом и откликом. Снабженный небольшой дополнительной про- граммой, клиент мог обмениваться с сервером более безопасным способом. NTLM использовалась для Всех продуктов NT вплоть до Windows NT 4, но в Windows 2000 появилась возможность использования аутентификации Kerberos — более защи- щенного средства, чем NTLM. Система аутентификации с вызовом и откликом была идеальной для изначаль- ных требований к LAN Manager. Но поскольку совместное использование ресурсов стало неотъемлемой частью концепции Microsoft для PC с Windows и серверов Windows NT, то стал необходим способ проверки правомерности доступа к таким ресурсам, как принтеры, файлы й внешние устройства. В идеале новая система ау- тентификации должна была работать сравнительно быстро, не предъявлять других требований, кроме добавления на клиенте некоторого совместимого програм- мн< го обеспечения, и гарантировать безопасность. Обладая таким продуктом, Microsoft могла бы конкурировать внутри привычной среды Windows с разделением ресурсов и внутренней безопасностью, которые предлагались в мире UNIX. Microsoft решила сетевую аутентификацию, используя протокол, который мог передаваться в существовавшие сообщения SMB (Server Message Block) уже при- менявшихся для совместного использования ресурсов. Необходимый пакет кли- ентского программного обеспечения содержал все сложные алгоритмы для генерации опознавательных признаков, используя новый протокол и встраивая этот набор в SMB-сообщения. Аутентификация с вызовом и откликом была необхо- дима, чтобы не допустить перехвата пароля в сети (хакер перехватывает сетевые сообщения, ищет в них пароли, а затем просто пользуется ими в своих целях). Пос- кольку системы аутентификации с вызовом и откликом используют разовые па- роли, их перехват не дает хакеру никакого преимущества. Более того, в целях предотвращения обратного проектирования значений вызовов и откликов и защи- ты основной информации для входа в систему, Microsoft обеспечила шифрование на сервере всех паролей входа. Естественно, слабые места в любой системе ау гентификации с вызовом и откли- ком неизбежны. В случае Microsoft основная проблема NTLM состояла не в самой системе аутентификации с вызовом и откликом, а в способе хранения паролей на сервере. Как оказалось, шифрование паролей, использованное в Windows NT, было слабым, и его довольно легко можно было взломать (в следующем разделе вы узнае- те, почему). В Windows 2000 система шифрования паролей была значительно уси- лена. Кроме того, в большинстве версий Windows возникали проблемы при использовании реестра. Реестр — это файл данных, поддерживаемый Windows для хранения информа- ции об оборудовании, установленных программах, пользователе, безопасности сис- темы и многих других аспектах машины. Одной из областей реестра является менеджер учетных данных в системе безопасности (Security Accounts Manager, или SAM) с записями для каждого пользователя, которому разрешен доступ к системе или к совместно используемым ресурсам. Естественно Microsoft делает все возмож-
386 , * Глава 11 ное, чтобы ограничить доступ к SAM и его содержанию. Атаки на собственно SAM даже стали частью повседневной жизни с появлением проекта Samba, который предназначен разрешить операционным системам, не относящимся к семейству Windows, совместно использовать ресурсы с Windows-машинами и наоборот. На- пример, при обращении с клиента Linux к серверу Windows нужно создать впечатле- ние для SAM сервера, что на другом конце находится машина Windows, от которой постуг ают правильные аутентификационные сообщения. Это неминуемо означает компрометацию SAM, и многие хакеры использовали механизмы Samba для взлома реестров на Windows NT Server. Шифрование в LANMAN LAN Manager (LANMAN для краткости) был предшественником NTLM и, следо- вательно, заслуживает рассмотрения, которое будет способствовать пониманию эволюции NTLM и Kerberos. Процесс, выполняемый программами LANMAN, легко объяснить в терминах хеширования пароля пользователя. При попытке клиента обратиться к ресурсу сервера пароль пользователя снача- ла преобразуется в 14-символьную строку. (Если пароль длиннее 14 символов, лиш- ние символы отбрасываются; если пароль короче 14 символов, к нему добавляются символы-заполнители, чтобы длина достигла 14 символов.) Затем все буквы преоб- разую .ся в прописные, поскольку система LAN Manager требует независимости от регистра. (DOS и первые системы Windows не зависели от регистра.) Затем клиент- ское программное обеспечение разбивает 14-символьный пароль на две семисим- вольные строки. (В Windows NT 14-символьная строка не разбивается на две, а трактуется как единое целое.) Каждая из двух семисимвольных строк используется как ключ для DES-шифрования 64-битовой константы. Затем эти две зашифрован- ные строки соединяются в одну строку, которая считается шифрованным паролем и сохраняется в SAM. (Когда клиент входит в систему, введенный им пароль шифрует- ся с использованием этого процесса и сравнивается с зашифрованной строкой в SAM; хранящаяся в SAM зашифрованная строка никогда не дешифруется.) Как упоминалось ранее, процесс шифрования, использованный в LANMAN, не настолько силен, как хотелось бы большинству пользователей. Причину легко по- нять, если выполнить несложные вычисления. Поскольку в LANMAN используется 14-символьный пароль из букв одного регистра, то всего существует 26 Л 14 комби- наций (если предположить, что пароль состоит только из букв английского ал- фавита). Чтобы представить это число, требуется 65 битов. Все бы хорошо, но существующий в LANMAN порядок разделения пароля на две семисимвольные строки ухудшает защиту. Вместо того чтобы взламывать 65-битовый ключ, хакеру нужно лишь справиться с двумя 32-битовыми ключами (дважды по 26 Л 7, получаем 32 бита). Хотя 32 бита дают все еще крепкий ключ, но он может быть атакован цик- лическими сценариями. Но задачка, стоящая перед хакером, оказывается еще проще из-за двух факто- ров: использования в LANMAN сцепленных строк и слабого требования к длине па- роля. Поскольку в LANMAN используются две семисимвольные строки, каждая из них может быть атакована отдельно. Если вторую строку взломать первой, она час- то может дать ключ к первой строке, к поиску ее в словаре. Например, если вторая строка расшифровывается как буквосочетание “tion”, за которым следует три нуле- вых символа (заполнители до 14-символьной строки), то очень быстро можно най- ти все слова с семью буквами перед “tion” и проверить их. Конечно, то же самое справедливо, если сначала взломать первую строку. Тогда легко протестировать вторую (если предположить, что для пароля используются обычные слова, присут- ствующие в словаре). Второе преимущество хакер получает, поскольку LANMAN не требует, чтобы пароли имели какую-либо конкретную длину. Большинство паролей
Протоколы аутентификации 387 пользователей в среднем имеют длину шесть или семь символов. В схеме LANMAN пароль дополняется до 14 символов нулями, а взломать нулевую строку легко (поэто- му атаку обычно начинают со второй семисимвольной строки). Если только полови- на строки в действительности содержит символы, то хакер, которому в противном случае пришлось бы повозиться с 32-битовыми ключами, обнаруживает, что про- цесс продвигается гораздо быстрее. Шифрование в Windows NT Хотя система шифрования пароля в LANMAN была довольно эффективна, она все равно была уязвима для атак хакеров. Поэтому в Windows NT была предпринята попытка усовершенствовать схему LANMAN в нескольких направлениях путем реа- лизации протокола входа в систему Windows (который появился в составе пакета клиентских платформ Windows 9Х). Протокол входа в систему Windows перехваты- вает все вызовы, используя пароль пользователя как ключ для генерации опознава- тельного признака. При входе пользователя в систему Windows преобразует пароль в 128-битовый клм ч (Windows отбрасывает открытый текст пароля и поддерживает пароль в за- шифрованной форме, пока пользователь не выйдет из системы). Когда иницииру- ется вход в систему или обращение к ресурсу, протокол входа в систему Windows обеспечивает генерацию сервером 64-битового (восьмибайтового) запроса. Клиент получает 64-битовый вызов и использует зашифрованный 128-битовый пароль пользователя для получения трех отдельных 56-битовых шифрованных алгоритмом DES представлений вызова. Затем эти три 56-битовые шифрованные строки объе- диняются в 168-битовую строку. Добавлением дефисов между подстроками форми- руется 24-байтовый отклик на вызов. Основной недостаток этой системы аутентификации с вызовом и откликом за- ключается в использовании шифрованных паролей. Если хакер может получить доступ к паролю, читая реестр или перехватывая сетевой трафик, содержащий шифрованный пароль, он может использовать шифрованный пароль для определе- ния отклика на вызов. (Открытый пароль пользователя не имеет значения для хаке- ра, поскольку в Windows при входе в систему используется только шифрованный пароль.) Как только хакер получает контроль над шифрованным паролем, он может имитировать на своей машине правомерного клиента и, пользуясь шифрованным паролем, отправлять корректные отклики на вызовы сервера. Может быть пред- принята и “лобовая” атака на открытый текст пароля, но она потребует гораздо больше времени и имеет меньше шансов на успех, чем в схеме аутентификации с вы- зовом и откликом, применяемой в LANMAN. Хакер повышает свои шансы, если может перехватить несколько удачных сообщений с вызовами и откликами, что по- зволяет ему проводить анализ откликов и потенциально строить более верные предположения об открытом тексте пароля. Аутентификация NTLM На рост интереса к средствам безопасности в 1990-е годы Microsoft ответила уси- лением защитных мер в каждой последующей версии LANMAN и Windows NT. В Windows NT 4 обеспечены три разные системы аутентификации, которые Mic- rosoft называет “локальной” (local), “доменной” (domain) и “удаленной” (remote). Аутентификация с вызовом и откликом используется в доменной и удаленной систе- мах, совместно эти системы обычно называют NTLM. Усовершенствование мер безопасности началось с собственно пароля пользова- теля. Вместо того чтобы использовать только прописные буквы, Windows NT-4-стах ла поддерживать Unicode, позволив включать в пароль гораздо большее число
388 Глава 11 симво 'OB, различающихся регистром клавиатуры. Хотя в Windows NT 4 продолжа- ло действовать ограничение в 14 символов, использование символов Unicode озна- чало, что строка пароля состояла уже из 28 байтов (14 двухбайтовых символов; Unicode требует два байта, чтобы представить все возможные символы). Более того, Microsoft отказалась от алгоритма DES в пользу распространяемого на рынке и по сути более сложного алгоритма Message Digest #4 (MD4). Для создания 128-би- тового шифрованного пароля, применяемого внутри системы (который часто на- зывается хешем NTLM), в Windows NT 4 используется MD4 и 28-байтовый пароль. К сожалению требования обратной совместимости навязали конструкции Win- dows NT 4 некоторые компромиссы. Вместо хеша NTLM фактически используются два шифрованных пароля: хеш NTLM и более старый хеш LANMAN. Оба они могут использоваться в процессе аутентификации с вызовом и откликом, поскольку кли- ент может генерировать отклики на оба вызова. Очевидно, взломать старый хеш LANMAN проще, поэтому хакеры сосредоточились на этой задаче и могли не зани- маться более сложным хешем NTLM. Контроль над хешем LANMAN по-прежнему давал им доступ к серверам и ресурсам Windows NT 4. Пытаясь залатать дыру в безопасности, Microsoft дополнила Windows NT 4 шиф- рованием элементов SAM. Новая система шифрования, названная “системным клю- чом”, использовалась для шифрования базы данных SAM, но ключ требовалось предоставлять при каждом обращении к SAM. Никаких проблем не возникало при включении машины (и однократном вводе ключа системным администратором), но получать ключ требовалось при каждой перезагрузке. Microsoft позволяла ад- министратору вводить ключ при каждой перезагрузке системы, что требовало физического присутствия администратора в любой момент, когда бы машины ни перезагружалась (проблема для удаленных систем). Дополнение в системе позво- лило помещать ключ на дискету, тогда он считывался при перезагрузке. Однако дискета также должна быть доступна при перезагрузке, что опять-таки приводило к проб темам для удаленных систем. Альтернативным решением было бы помеще- ние ключа в реестр с возможностью автоматической загрузки и дешифрования SAM. Недостаток этого подхода очевиден: кто угодно может скопировать реестр и получить ключ. Kerberos При работе NTLM и других систем аутентификаций с вызовом и откликом кли- ентская машина должна общаться непосредственно с сервером, чтобы идентифици- ровать себя на сервере. Такой подход работает хорошо в некоторых архитектурах, но не слйшком эффективен, когда имеется много разных машин, время от времени 'взаимодействующих друг с другом, не возлагая на себя роль сервера. В этом случае более удобна модель, при которой обе взаимодействующие между собой машины могут идентифицировать себя на третьей машине, играющей роль единого сервера аутентификации. По этому принципу работают центры распределения ключей (Key Distribution Centers, или KDC). и этот принцип заложен в основе системы Kerberos. Аутентификация Kerberos используется в Windows 200Q и может быть включена в другие операционные системы. Kerberos — система, базирующаяся на ключах, предоставляющая возможности устойчивой аутентификации. Прежде чем перейти к конкретному рассмотрению этой системы, в общих чертах познакомимся с аутен- тификацией, базирующейся ца ключах, и узнаем, как возникла система Kerberos. Центры распределения ключей Ключи являются неотъемлемой частью алгоритмов шифрования. Проблема ключей шифрования состоит в том, что кто-то (или что-то) должен ими обладать
Протоколы аутентификации 389 и предъявлять их при необходимости (в надлежащем процессе аутентификации, чтобы гарантировать допустимость запроса). Статические ключи шифрования, очевидно, менее надежны, чем периодически меняющиеся ключи. Смена ключа часто уменьшает шансы какого-либо постороннего лица завладеть ключом и исполь- зовать его для доступа в дальнейшем. Чтобы обеспечить динамические ключи, доступные по требованию, в 1980-е годы в банковском деле возникли центры рас- пределения ключей. Это были машины, предоставлявшие ключи по запросам любо- му авторизованному пользователю. Основные преимущества KDC заключались в том, что они могли использоваться любым числом сайтов, не требовали физического распределения ключей по раз- ным территориям и обеспечивали постоянно изменяемые ключи. У каждой маши- ны есть уникальный мастер-ключ (master key), который используется как для аутентификации KDC, так и в качестве средства шифрования временной информа- ции ключа, передаваемой между машиной и KDC, а также другим машинам, кото- рым нужен тот же самый временный ключ. На рисунке показан процесс в KDC: 1. В любой момент, когда двум машинам нужно взаимодействовать, одна из них получает из KDC случайный, временный ключ шифрования, используя свой мастер-ключ, чтобы идентифицировать себя и зашифровать трафик между ней и KDC. 2. Сервер отправляет в ответ два временных ключа шифрования: один, зашифрованный мастерключом машины-инициатора, и еще один, зашифрованный мастерключом второй машины. Эти сообщения от KDC, содержащие временные ключи, называются “мандатами” (tickets). 3. Машина, начавшая процесс, отправляет второй машине мандат, содержащий копию временного ключа второй машины (зашифрованную мастер-ключом второй машины). 4. Получающая машина дешифрует мандат, используя свой мастер-ключ. После завершения сеанса ключ становится ненужным, и для следующего сеанса от KDC нужно получить новый ключ. Поскольку KDC отправляет два мандата, зашифрованные мастер-ключами обеих машин, то на KDC нужно поддерживать главную базу данных всех мастер-ключей. Ни одна машина никогда не знает мастерключа другой машины.
390 Глава С этим процессом связана одна основная проблема: у машины, запускающей про- цесс, нет способа узнать, действительно ли мандат для машины В предназначен для нее или он будет направлен другой машине, которая выдает себя за машину В. Кроме того, на уровне пользователя невозможно контролировать доступ к KDC и другим машинам, использующим KDC. Для разрешения этой проблемы был разработан протокол Нидхэма-Шредера (Needham-Schroeder), позволяющий включить в KDC систему аутентификации с запросом и отзывом — рисунок иллюстрирует этот протокол: В этом протоколе происходит следующее: 1. Инициирующая машина отправляет KDC запрос, который включает ее собственный мастер-ключ (для аутентификации инициирующей машины) идентификатор пользователя, инициирующего процесс, идентификацию машины-получателя и сгенерированный случайный вызов. 2. В этом случае КОС возвращает мандаты с временными ключами для инициирующей и получающей машин и вызов, зашифрованный мастер-ключом инициирующей машины. 3. Инициирующая машина дешифрует сообщение и проверяет вызов, а также идентификацию получающей машины (для предотвращения имитации), а затем переправляет получающей машине ее временный ключ вместе с уникальным кодом вызова. 4. Получающая машина использует свой мастер-ключ для дешифрования сообщения от КОС и получения временного ключа для сеанса. В сообщении для машины-получателя также имеется идентификатор инициирующего пользователя, который используется для генерации сообщения вызова, шифруемого временным ключом; этот вызов отправляется обратно инициирующему пользователю. 5. Получив вызов, программы на машине пользователя дешифруют его код, вычитают из него единицу и шифруют его снова для повторной передачи получателю. 6. Наконец машина-получатель дешифрует ответ, проверяет, что единица вычтена (это было бы невозможно без временного ключа), и предполагает, что все в порядке. Пользователь проверен (в теории), обе машины получили временные ключи и имитация невозможна (опять-таки в теории).
Протоколы аутентификации 391 Теория разваливается, когда хакер перехватывает одно из сообщений, летаю- щих между отправителем и получателем. Не слишком серьезной лобовой атакой ха- кер может дешифровать ключ сеанса. Получив ключ сеанса, любая машина может затем использовать тот же самый ключ для продолжения диалога с машиной-получа- телем даже после завершения сеанса инициирующей машиной. Чтобы этого не до- пустить, было предложено включить в протоколы временные метки (timestamps), заменяющие или дополняющие шифрованные данные. KDC системы Kerberos Система Kerberos первоначально была разработана в Массачусетском техноло- гическом институте и включала протокол Нидхэма-Шредера и идеи временных ме- ток, предложенных Деннинг (Denning) и Сакко (Sacco). Эта система прошла четыре итерации, пока в 1989 г. не была выпущена версия 4 Kerberos. В настоящее время ис- пользуется следующая версия, Kerberos 5. KDC Kerberos состоит из нескольких отдельных серверов, выполняющих раз- личные функции. На “сервере аутентификации” для выдачи мандатов используется протокол, аналогичный протоколу Нидхэма-Шредера. Чтобы получить мандат от сервера аутентификации, машина отправляет запрос “KRB_AS_REQ” (Kerberos Authentication Server Request). Это сообщение включает имя инициирующей маши- ны, имя пользователя, имя, машины получателя, уникальный код и интервал допус- тимости, указывающий, когда временный ключ должен считаться допустимым и когда его срок истекает. Сервер аутентификации генерирует случайный временный ключ и возвращает его в сообщении “KRB_J\S_REP” (Kerberos Authentication Server Reply), включающем также мандат для получающей машины. Далее инициирующая машина конструирует сообщение “KRB_AS_REQ” для по- лучающей машины. Оно содержит шифрованный мандат от сервера аутентифика- ции, имя инициирующего пользователя и временную метку. Имя пользователя и временная метка шифруются временным ключом. Получающая машина дешифру- ет мандат, переданный сервером аутентификации, и использует содержащийся в нем временный ключ для дешифрования имени пользователя и временной метки от инициирующей машины. Имя пользователя в мандате сервера аутентификации должно совпадать с именем пользователя, полученным от инициирующей машины, а временная метка должна быть сделана относительно недавно (обычно не более пяти минут назад). В Kerberos нашло свое развитие решение другой проблемы с ключами — хране- ние мастер-ключа. Мастер-ключи поддерживаются на KDC и используются машина- ми, которым требуется получать обслуживание от KDC. Это значит, что любой мастер-ключ, находящийся в какой-либо машине, может попасть посторонним лю- дям. Очевидное решение состоит в том, чтобы получать мастер-ключ от пользовате- ля, когда он входит в систему или хочет использовать Kerberos, а затем потерять ключ так быстро, как только возможно, чтобы не допустить перехвата. Поскольку запрашивать у пользователя ключ каждый раз, когда он необходим, не слишком удобно с точки зрения пользователя, требуется другой способ обработки мас- тер-ключей. Решение заключается в создании из мастер-ключа временного ключа и отбрасывании мастер-ключа. После этого временный ключ (называемый “сеансо- вым ключом”) используется для всех взаимодействий с KDC. Для обработки сеансовых ключей в Kerberos используется другой сервер. Он на- зывается “сервером выдачи мандатов ” (ticket-granting server) и принимает “мандаты на выдачу мандатов” (ticket-granting tickets). Когда пользователь входит в систему на своей машине, мастер-ключ используется для контакта с сервером аутентификации, который выдает мандат на выдачу мандата, используя для шифрования мас- тер-ключ. После дешифрования ответа мастер-ключ выбрасывается. Затем мандат
392 Глава на выдачу мандата используется для соединения с сервером выдачи мандатов, для получения доступа к любому сервису, необходимому машине. Каждый раз, когда пользователю нужен новый сервис, для обращения к серверу выдачи мандатов ис- пользуется временный мандат до тех пор, пока пользователь не выйдет из системы и временный мандат не станет недействительным. Процесс обработки “мандат на выдачу мандатов” является вариацией на тему только что рассмотренных сообщений Kerberos. Когда пользователь отправляет за- прос серверу аутентификации, он получает в ответ временный сеансовый ключ в формате мандата на выдачу мандата. Для соединения с сервером выдачи мандатов, исполх зуется новое сообщение “KRB_TGS_REQ” (Kerberos Ticket-Granting Server Request). Оно содержит имя пользователя и временную метку, мандат на выдачу мандата, период допустимости, уникальный код и имя получателя. Сервер выдачи мандатов дешифрует ключ мандата временным ключом и проверяет компонент до- пустимости. Затем этот сервер создает сеансовый ключ и отправляет его обратно на инициирующую машину вместе с копией, зашифрованной для получающей маши- ны, в сообщении “KRB_TGS_REP”. До сих пор основное внимание уделялось аутентификации пользователя. Кроме этого, Kerberos включает программы идентификации машин. Протоколы Kerberos, которые были рассмотрены, не поддерживают уникальные ключи для каждой ма- шины, поэтому аутентификация ограничивается уровнем пользователя. Это пра- вильно, так как доступ к ресурсам ббычно связан не с уровнем машйны, а с уровнем пользователя. Это также дает возможность пользователям входить в систему на лю- бой машине и выполнять свою работу. Однако такой подход все-таки не лишен своих недостатков, особенно там, где отдельные PC или рабочие станции закрепля- ются за конкретными пользователями. На таких машинах локальные файлы будут специфичными для каждой машины, поэтому аутентификация не проверяет, что пользователю конкретной машины разрешен доступ к этим файлам. Чтобы ограни- чить доступ к локальным ресурсам, требуется предварительная аутентификация (или локальная аутентификация, если пользоваться терминологией Windows NT). Предварительная аутентификация была включена в Kerberos 5, чтобы разре- шить аутентификацию пользователей или машин, отправляющих запросы в KDC, вместо того чтобы полагаться на КОС в идентификации источника. При использо- вании предварительной аутентификации рабочая станция будет запрашивать мас- тер-ключ (пароль) пользователя до выполнения любых попыток соединения с КОС. (Это отличается от того, что было ранее, когда КОС мог отправлять ответ на за- прос, не требуя мастер-ключ.) При использовании предварительной аутентифика- ции начальный запрос в КОС несколько отличается, поскольку для шифрования запроса, содержащего временную метку, может использоваться мастер-ключ. Сер- вер получает эти запросы и проверяет свою внутреннюю базу данных, чтобы узнать, требуется ли предварительная аутентификация. Если это так, КОС дешифрует за- прос при помощи мастер-ключа, проверяет срок действия по временной метке и за- вершает запрос. В Kerberos 5 также были включены еще Две возможности: пересылаемые (for- wardable) и уполномочивающие (proxiable) мандаты. Пересылаемый мандат означа- ет, что рабочая станция может запросить мандату на выдачу мандатов, связанный с другой сетью (называемой “областью” (realm) в терминологии Kerberos'). Уполно- мочивающие мандаты позволяют рабочей станции запрашивать мандаты, действи- тельные на машинах другой сети. Kerberos идентифицирует пользователей других сетей, организуя взаимодействие между KDC с использованием так называемых справочных (referral) мандатов.
Протоколы аутентификации 393 Windows 2000 и Kerberos В Windows 2000 методы аутентификации NTLM заменены методами Kerberos. Во всех запросах к файлам и каталогам на удаленных машинах, работающих под управлением Windows 2000, используются мандаты. Когда вы входите в систему на сервере или рабочей станции Windows 2000, мандат на выдачу мандатов получается от сервера, хранящего все мастер-ключи в Active Directory. Между Kerberos и реализацией в Windows 2000 имеются некоторые различия, хотя Microsoft претендует на полное взаимодействие в соответствии со стандарта- ми Kerberos. Это важно, чтобы позволить машинам, работающим под управлением других систем, взаимодействовать с машинами Windows 2000. (Windows 2000 под- держивает с целью обратной совместимости протоколы LANMAN и NTLM, но они значительно уступают протоколам Kerberos.) В Windows 2000 каждая машина счита- ется самостоятельной единицей и мастер-ключи для этой машины (а также мас- тер-ключи вошедших в систему пользователей) хранятся в системе. Windows 2000 начинает работу с Kerberos через трехшаговый процесс, дополня- ющий обычную процедуру входа в систему: 1. Процесс начинается с окна Winlogon, собирающего имя пользователя и пароль, которые передаются авторизатору локальной безопасности (Local Security Authority, или LSA). 2. LSA преобразует пароль в мастер-ключ Kerberos, затем передает имя пользователя и хешированный мастер-ключ провайдеру поддержки безопасности (Security Support Provider, или SSP). 3. SSP взаимодействует с серверами Kerberos и пытается получить мандат на выдачу мандатов, используя сообщения предварительной аутентификации, созданные по матер-ключу пользователя. (Если доступные серверы Kerberos отсутствуют, по умолчанию SSP использует протокол NTLM.) Безопасность в .NET и Windows Компания Microsoft считает безопасность важнейшим компонентом среды .NET Framework и включила в нее сложный механизм безопасности. Для большинства приложений имеющаяся в .NET Framework безопасность избыточна: большинство разработчиков придут к заключению, что в действительности им будет достаточно довольно простых встроенных процедур. Средства безопасности в .NET Framework реализуются следующими простран- ствами имен: □ System Security — встроенная структура, обеспечивающая безопасность в.NET □ System Security. Cryptography — содержит три отдельные пространства имен, обеспечивающие службы криптографии и аутентификации □ System. Security. Permissions — классы управления доступом к ресурсам о System. Security. Policy — группы кода и условия членства, используемые CLR для обеспечения соблюдения политики безопасности □ System.Security. Principal — классы, представляющие контекст безопасности программы
394 Глава 11 Из этих пространств имен четыре, не входящие в System. Security. Cryptography, используются разработчиками наиболее широко. Однако прежде чем мы обратимся к использованию пространств имен, мы должны понять, как аппарат безопасности .NET взаимодействует со средствами безопасности Windows, которые мы обсудили ранее в этой главе. Кбгда приложение .NET пытается получить доступ к такому ресурсу, как файл или каталог, CLR проверяет действующую политику безопасности на исполнитель^ ном компьютере. Windows NT позволяет реализовать политики безопасности на уровне пользователей (с детальным указанием разрешений и прав каждого пользо- вателя в форме разрешенных и запрещенных действий) или на уровне машин. В .NET Framework применяется политика безопасности, называемая “безопаснос- тью доступа к коду” (Code Access Security, или CAS), которая является иерархически расширяемой моделью, состоящей из “групп кода”. Группы кода можно установить по таким разным критериям, как имена сайтов, имена издателей или разработчи- ков, имена сетевых зон и т. д. Существует много разных критериев, которые можно установить для групп кода, и каждая группа имеет связанный с ней набор разреше- ний. Допустимы следующие критерии групп кода: □ Application directory—местный каталог приложения О Cryptographic hash — хеш, используемый при шифровании □ Custom — условие, определенное в приложении О File — доступ к файлам О Net — сетевая идентификация для собственной сети приложения □ Software publisher — подпись издателя по технологии Authenticode □ Strong name — строгое имя .NET □ URL —URL кода □ Web site — Web-сайт кода □ Zone — зона кода Безопасность ресурсов .NET Microsoft спроектировала безопасность среды .NET Framework для работы с уже существующими средствами безопасности операционных систем Microsoft Win- dows, особенно с Windows 2000 и Windows NT. Общеязыковая среда выполнения приложений .NET работает с пользователями и группами Windows NT, обеспечивая управляемый доступ к ресурсам. Любые приложения, написанные по стандартам .NET, могут задавать любые необходимые им типы доступа, а затем политика безо- пасности операционной системы должна определить, можно ли выдать эти раз- решения. Для доступа к ресурсам используется несколько классов, и все они порождены от базового класса System. Security.CodeAccessPe г mission. Его подкласса- ми, которые обычно называют классами разрешений доступа к коду, являются: Класс Описание DirectoryServicesPermission Дает доступ к классам System. Directoryservices DnsPe mission Дает доступ к DNS Envi ronmentPe mission Дает доступ к переменным окружения
Протоколы аутентификации 395 продолжение таблицы EventLogPermission Дает доступ к журналу событий FileDialogPe mission Дает доступ к файлам, выбранным в диалоговом окне FilelOPe mission FileOpen Дает доступ к файлам в режимах чтения, записи и дополнения IsolatedStorageFilepemission Дает доступ к системам виртуальных файлов IsolatedStoragePemission Дает доступ к памяти, выделенной конкретным пользователям MessageQueuePemission Дает доступ к службе сообщений OleDbPe mission Дает доступ к базам данных с использованием OLE DB PerfomanceCounterPemission Дает доступ к счетчикам производительности PrintingPemission Дает доступ к принтерам ReflectionPemission Дает доступ к информации времени выполнения Regist ryPe mission Дает доступ к реестру SecurityPe mission Дает доступ к выполнению кода, установлению разрешений и прав Se rviceCont rolle rPe mission Дает доступ к службам Windows SocketPermission Дает доступ к сокетам SqlClientPe mission Дает доступ к базам данных SQL UlPe mission Дает доступ к средствам пользовательского интерфейса (диалоговым окнам, буферу обмена и т. д.) WebPermission Дает доступ к Web-соединениям Базовый класс System.Security.CodeAccessPemission также включает несколько методов, используемых для реализации безопасности. Они показаны в следующей таблице: Метод Описание Assert() Обеспечивает доступ кода к ресурсу Сору() Создает копию объекта Demand() Определяет, все ли вызывающие программы имеют разрешение Deny() Отказывает в доступе вызывающим программам, находящимся выше по стеку FromXml() Восстанавливает объект разрешения из XML-кода
396 Глава 11 продолжение таблицы IntersectO Создает разрешение для логического пересечения двух других объектов разрешения IsSubsetOfO Определяет, является ли один объект разрешения подмножеством другого объекта PermitOnlyO Ограничивает разрешения вызывающими программами выше по стеку RevertAll() Статический метод — отменяет все переопределения разрешения RevertAssertO Статический метод—отменяет действие всех предыдущих методов Assert RevertDenyO Статический метод — отменяет действие всех методов Deny RevertPermitOnlyO Статический метод — отменяет действие всех методов Ре rmitOnly ToStringO Преобразует объект разрешения в строку ToXmlO Преобразует объект разрешения в XML Union() Создает разрешение из объединения двух других объектов разрешения Методы Assert(), Demand() и Deny() используются для реализации проверки раз- решений в коде при выполнении. Для обработки комбинации двух разрешений можно воспользоваться методом IntersectO или методом Union(). Здесь приведен пример кода, показывающий, как применять эти методы для получения доступа к ка- талогу с \Networking\Authentication\codetemp: using System; using System.Security; using System.Security.Permissions; public class FilelOPDemo = - public static void MainCStringll args) i FilelOPermission myPerrn- = new . * «. FileIOPermission(FileIOPermissionAccess AllAccess, @’c;\Networking\Autbentication\codetemp"); SecutityElement mySec = myPerm.ToXml(); Console.wViteLine(mySec. ToStringO); . ‘ ' -• JL ЗгС? 1 *“ k.j X v -ХЧг Объект FilelOPermission используется для создания объекта с полным доступом к целевому каталогу, а вызов метода ТоХтК) — для возвращения объекта Se- es rityElement, который преобразуется в строку и выводится на консоль. В коде можно использовать методы класса System. Security. CodeAccessPe mission для запроса доступа к ресурсу. Класс FilelOPermissionAccess контролирует следую- щие разрешения: □ AllAccess — полный доступ к файлу или каталогу □ Append — дополнительное разрешение для файла или каталога □ NoAccess — нет доступа к файлу или каталогу
Протоколы аутентификации 397 □ PathDiscovery — информация о пути □ Read — доступ на чтение к файлу или каталогу □ Write — доступ на запись в файл или каталог Эти разрешения используются как флаги в переменной FilelOPermissionAccess. Например, для получения доступа на чтение файла c:\codetemp\data. dat можно на- писать следующий код: using System; >- using System.Seen rity; using System.Security.Permissions; . public class FileI0PDemo2 : { v. public static void Main(String[] args) FilelOPermissionAccess myPermsAcc = FilelOPermissionAccess.Read; FilelOPermission myPerm = new FilelOPeemission(myPermsAcc, 1 c:\Networking\Authentication\data.dat'); try иЦ? myPerm.Demand(); } ’ ’ catch (SecurityException res) Console.WriteLineU*Sorry, no access."); Л; ' f ; Аналогичный код используется для методов DenyO и Assert(). Чтобы отказать в доступе к ресурсу с целью не допустить его перезаписи (или по каким-либо другим соображениям), можно изменить разрешения, установленные по умолчанию, как в следующем коде, где запрещается доступ к файлу с: \codetemp\data. dat: PemissionSet myPerm » new PermissionSet(Permissions!ate.None); myPerm.AddPermission(new FileIOPermission(FileIOPermissionAccess AllAccess, c .\Networking\Authentication\data. dat ");; myPerm. Deny(); .* • ’ .» >. * V Чтобы снять разрешение Deny, можете использовать статический метод Re- vertDenyO: CodeAccessPemission. RevertDeny(); Второй набор классов, производных от базового класса System. Security.Code- AccessPermission, это — классы разрешений идентичности. Эти классы включают такие характеристики приложения, как цифровые подписи, расположение храни- лища и другие. При выполнении CLR использует эти характеристики, чтобы пред- оставлять разрешения идентичности. К ним имеют отношение следующие классы: □ PublisherldentityPemission — цифровая подпись издателя □ Sitelden t it у Ре mi ssion — сайт, содержащий приложение О StrongNameldentityPemission — строгое имя □ URLIdentityPermission — порождающий URL □ ZoneldentityPe mission — порождающая зона безопасности
398 Глава 11 Обычно имени приложения, номера версии и некоторой дополнительной ин- формации, определяющей локальную машину, бывает достаточно для идентифика- ции приложений, но не для целей безопасности. Концепция строгого имени была введена, чтобы включить всю эту базовую информацию, а также открытый ключ шифрования и цифровую подпись. Поскольку в процессе создания строгого ключа используются контрольные суммы, становится возможным обнаружение измене- ний в содержании приложения. Кроме того, использование закрытого ключа гаран- тирует, что не будет допущена имитация конкретной машины. В своем приложении вы можете использовать пространство имен Sys- tem Security Permissions с классами и объектами из System, Net. Например, если нуж- но проверить разрешения доступа к файлу, следует вызывать класс FilelOPemission для каждого выполняемого обращения (не только первого). Любая неудача при вы- зове приведет к порождению исключения SecurityException. Основанная на ролях безопасность .NET Основанные на ролях средства безопасности, которые определяют, имеет ли данную роль пользователь, выполняющий приложение .NET, могут также помочь определить идентичность пользователя. Все средства безопасности .NET, связан- ные с ролями, содержатся в классе PrincipalPermission. Можно использовать этот класс, создав объект класса и вызвав его метод Demand(). Если активный участник (пользователь и роль) не соответствует пользователям и ролям, содержащимся в этом объекте, порождается исключение SecurityException, и метод Demand() завер- шается аварийно. Для выполнения декларативной проверки безопасности нужно добавить ат- рибуты, обеспечивающие для приложения пользователей и роли. Неудача при сравнении пользователя, выполняющего код, вызовет аварийное завершение при- ложения. Интерфейс System.Net.lAuthenticationModule Интерфейс System. Net. ZAuthenticationModule устанавливает свойства и методы об- работки клиентской аутентификации в приложениях .NET. Свойство Sys- tem. Net ZAuthenticationModule AuthenticationType — это нечувствительная к регистру строка, указывающая протокол, реализованный модулем. Значения этой строки ре- зервируются для использования модулями, реализующими указанные протоколы: □ Basic — базовая аутентификация (определенная в RFC 2617 IETF) □ Digest — аутентификация на основе дайджестов (определенная в RFC 2617 IETF) □ Kerberos — аутентификация Kerberos (определенная в RFC 1510 IETF) Другое свойство, ZAuthenticationModule. CanPreAuthenticate, указывает, возможна ли для этого кода предварительная аутентификация. В классе System Net. ZAuthenticationModule содержатся два метода: □ ZAuthenticationModule. AuthenticateO — возвращает экземпляр класса Authori- zation, обеспечивающий ответ на вызов аутентификации О ZAuthenticationModule PreAuthenticateO — возвращает экземпляр класса Autho- rization, содержащий информацию об аутентификации клиента Все, что реализует интерфейс System.Net.ZAuthenticationModule, называется модулем аутентификации. Каждый модуль, зарегистрированный менеджером аутентификации, должен иметь уникальное значение System Net. ZAuthenticationMo-
Протоколы аутентификации 399 dole.AuthenticationType. Модуль аутентификации регистрируется менеджером аутентификации через вызов метода System.Net.AuthenticationManager,RegisterO и передачу ему вашего модуля аутентификации. Когда к менеджеру аутентификации поступает запрос аутентификации, зарегистрированные модули аутентификации получают возможность обработать этот процесс в их собственном методе Sys- tem. Net.lAuthenticationModule.Authenticate(). Менеджер аутентификации ищет модуль аутентификации, вызывая метод System. Net. lAuthenticationModule.Authenti- cate() или System Net.lAuthenticationModule.PreAuthenticateO для каждого зарегистрированного модуля в порядке их регистрации. Как только модуль возвращает экземпляр класса Authorization (указывает, что может обработать аутентификацию), менеджер аутентификации прекращает дальнейший поиск. Если клиент хочет избежать ожидания поступления от сервера запроса аутентификации, он может при обращении к серверу запросить информацию пред- варительной аутентификации, используя свойство System.Net. lAuthenticationModu- le. CanPreAuthenticate зарегистрированного модуля. Если это свойство возвращает значение true, модули получают возможность предоставить информацию предвари- тельной аутентификации. Имитация Нам осталось рассмотреть последний аспект, который называется имитацией (impersonation). По умолчанию это средство отключено. Имитация может оказать- ся полезным приемом, если вы не хотите кодировать в приложении .NET объемные программы аутентификации, а решили выполнение всей этой работы поручить платформе Windows. С другой стороны, если вы хотите сами написать специфичес- кую аутентификацию, имитация вам не потребуется. При имитации приложение .NET выполняется, используя удостоверенный идентификатор клиента. Прежде чем вы успеете подумать, что ведь обычно все так и происходит, будет полезно привести краткое пояснение. Например, когда IIS удостоверяет пользователя, запрашивающего страницу ASP.NET, он передает об- ратно в приложение идентификатор “local machine”. В обычной конфигурации идентификатор “local machine” имеет полный доступ ко всем каталогам и файлам, оставляя проведение авторизации для доступа другим механизмам (например, URL-авторизацию). При подключении средства имитации приложение .NET вмес- то идентификатора, полученного от IIS, приобретает идентификатор реального пользователя. Например, если пользователь "tparke г" пытается получить доступ к ресурсу, обычно IIS проверяет пользователя и возвращает “local machine” в качест- ве идентификатора для использования в приложении .NET. При включенном сред- стве имитации вместо этого используется идентификатор “tparker”. Достоинство этого приема состоит в том, что при имитации конкретного пользователя для огра- ничения доступа могут использоваться ACL (что невозможно для “локальной маши- ны", поскольку этот идентификатор обычно дает полный доступ). Имитацию можно еще представить себе так. Когда она отключена, a IIS надлежа- щим образом удостоверил пользователя, то, если приложение ASP.NET пытается получить доступ к каталогу, Windows считает, что к каталогу пытается обратиться пользователь “локальной машины”, и предоставляет ему доступ. Если же имитация включена, приложение .NET трактуется как пользователь, который пытается полу- чить доступ к ресурсу, поэтому для получения разрешения в обычной ситуации Windows проверяет ACL. (Исключение составляет использование информации конфигурирования: даже имитируя пользователя для чтения конфигурационных файлов, приложение будет использовать идентификатор “local machine”, иначе оно может блокироваться.)
400 Глава 11 В ASP.NET для подключения имитации нужно добавить в файл web.config новую строку с тегом identity: <configiratjLon> <s/stem. eb> - «identity impersonate-"true" />• «/system.web> «/configuration? Можно также применить незначительное ухищрение, чтобы приложения ASP.NET при имитации всегда использовали идентификатор конкретного пользо- вателя. Для этого нужно указать в строке identity имя пользователя и пароль (ис- пользуемые для аутентификации): е «configtration? «system web? J - ' «/configuration?. ------ -- -.. “* ta* й«identity impersonate="true" username^"tparkerpassword=”secret" /> «/system, wet)? ГИЯжОr •J®. Побеспокоившись о единственном пользователе, вам не придется устанавли- вать ь а Web-сайте всех ACL для каждого пользователя или каждой группы, и вы сэко- номите массу времени. Итоги Из этой главы вы узнали, как Windows может обрабатывать аутентификацию пользователей, а также познакомились с авторизацией для доступа к ресурсам. Мы рассмотрели: □ LANMAN (Microsoft LAN Manager) О NTLM (Windows NT LAN Manager) □ Kerberos Мы рассмотрели в .NET безопасность ресурсов и безопасность, основанную на ролях, и вкратце познакомились с интерфейсом System. Net. lAuthenticationModule.
Программист - программисту -NET Сетевое программирование для профессионалов Сетевая организация ПО — одна из центральных задач программи- рования при разработке бизнес-приложений. Прочитав книгу, вы сможете уверенно программировать сетевые задачи в .NET и будете понимать основные протоколы. В настоящий момент набор протоколов, поддерживаемый классами .NET, ограничен на транспортном уровне протоколами TCP и UDP, а на прикладном уровне протоколами HTTP и SMTP. Подробное об- суждение этих тем дополняется примерами реализации в NET про- токолов прикладного уровня, поэтому книга представляет чрезвы- чайно большой интерес для тех, кому требуется использовать прото- колы, не поддерживаемые в настоящее время платформой .NET, а также для желающих пользоваться поддерживаемыми протоколами. Основные темы книги: • Обзор архитектуры физических сетей • Сетевые протоколы и модель OSI • Программирование сокетов в .NET • TCP, UDP и сокеты групповой рассылки • Реализация протоколов прикладного уровня на примере FTP • Интернет-программирование и классы .NET для электронной почты • Реализация клиентов POP3 и NNTP для чтения из почтовых ящиков и групп новостей • Защита сетевого обмена в .NET Необходимый уровень подготовки читателя От читателей не требуется владение сетевым программированием, но те, кто знаком с использованием сетей в другой среде, смогут освоить книгу довольно быстро и обязательно найдут в ней немало ценных сведений Код всех примеров в книге написан на С#, поэтому предполагается, что читатель имеет опыт работы на этом языке программирования. Расширьте свои знания и продвиньтесь по служебной лестнице Wrox Press Inc., 29 S. LaSalle St, Suite 520, Chicago, Illinois 60603. USA Wrox Press Ltd., Arden House, 1102 Warwick Road, Acocks Green, Birmingham. B27 6BH. UK Издательство "ЛОРИ" www.lory-press.ru