001 Системный Администратор 10 2002

Page 1

№1 октябрь 2002 журнал для профессиональных системных администраторов, вебмастеров и программистов

Сканер портов: пример реализации Программирование сокетов

Настройка почтового сервера: postfix+imap+mysql Что такое SAMBA? Python+администрирование

Философия Perl «Стеммер» — программа морфологического анализа Оптимизация работы с памятью в Perl Java – магия отражения


БЕСПЛАТНАЯ РЕКЛАМА БЕСПЛАТНОГО ПРОДУКТА WWW.KERNEL.ORG


Давайте знакомиться — «Системный администратор». В октябре вышел в свет первый номер нового журнала, который, как я надеюсь, облегчит жизнь не только системным администраторам, но и программистам сетевых и серверных приложений, вебмастерам, любознательным студентам и всем тем, кто не мыслит своей жизни без Интернета. В этом номере Вы найдете полезные советы, касающиеся программирования на языках Perl и Java. Ведущий разработчик системы «Рамблер» Андрей Коваленко делится своим опытом в области морфологического анализа. Даниил Алиевский откроет секреты магии Java в статье «Java - магия отражений». С помощью материала Вячеслава Калошина Вы сможете своими руками установить и настроить удобный почтовый сервер. Проектировщикам и студентам старших курсов будет интересна рубрика «Образование» — в ней представлен обширный теоретический материал-исследование, посвященный проектированию реляционных баз данных. Роман Сузи решает некоторые задачи администрирования с помощью языка Python. Современные дамы настроены не менее решительно: удастся ли им отвоевать место в нише компьютерных «гуру», давно и весьма основательно занятой мужчинами? Обо всем об этом — на страницах первого номера. Впрочем, все что Вы будете читать - это результат творческой деятельности авторов. А вопрос, почему они используют тот или иной продукт, Вы можете задать им лично на форуме нашего сайта www.samag.ru . Не сомневаюсь, что и Вам есть чем поделиться, поэтому ждем Ваших статей и интересных материалов. Успехов! Искренне Ваш, Александр Михалев


СОДЕРЖАНИЕ

СОДЕРЖАНИЕ

АДМИНИСТРИРОВАНИЕ ПРОГРАММИРОВАНИЕ Хеви Хардвэр Установка и настройка коммутаторов CISCO CATALYST серий 2900XL и 3500 Всеволод Стахов 8-13 Удобная почтовая система Вячеслав Калошин В статье «Удобная почтовая система» подробно описывается установка и настройка почтового сервера на базе связки postfix+imap через СУБД mySQL.

14-19

Миграция с Windows на Linux Дмитрий Галышев Автор материала «Миграция c Windows на Linux» рассматривает последовательность действий, необходимо для переноса файлового сервера из Windows NT на Linux и требования к установке SAMBA-сервера.

20-23

Что такое SAMBA? Сергей Еремчук 24-27 Python в администрировании сервера: почему бы и нет? Роман Сузи 28-32 2

Работа с текстом, или философия Perl Коновалов Евгений 34-41 Эффективное использование памяти в Perl при работе с большими строками Даниил Алиевский 42-45 «Стеммер» Морфологический анализ для небольших поисковых систем Андрей Коваленко 46-49 Java - магия отражений Даниил Алиевский 50-57 ColdFusion или, возможно, лучшее решение для создания динамических сайтов Александр Меженков 58-61 Программирование сервисов в Windows 2000 Всеволод Стахов 62-66


СОДЕРЖАНИЕ

СОДЕРЖАНИЕ

СЕТИ

ОБРАЗОВАНИЕ Взаимные функциональные зависимости Андрей Филиппович

Анализатор сетевого трафика Владимир Мешков 68-71 Сканер портов: пример реализации Владимир Мешков «Сканер портов» - Целью данной статьи является описание принципов функционирования и внутреннего устройства простого сканера портов TCP протокола.

72-76

«Взаимные функциональные зависимости» — исследование проведено по наиболее распространенным учебникам, посвященным проектированию реляционных баз данных. Рассматриваются ошибки и нетривиальные моменты, освещаемые в подобной литературе.

84-89

Программирование сокетов Всеволод Стахов 78-82

CommerceML - стандарт обмена коммерческой информацией в формате XML Елена Ртищева 90-93

ТЕНДЕНЦИИ 4-5

FAQ PERL 6, 77

№1, октябрь 2002

Женщина и компьютер Уступите место женщине Евгения Саблина 94 Почему мало женщин в компьютерных компаниях Вячеслав Михалев 95 Анонс 96 3


тенденции Дырка в PGP 7.1 В PGP обнаружилась очень неприятная дырочка: при обработке длинных имен файлов в шифрованном архиве происходит переполнение буфера, со всеми вытекающими последствиями. Патчик к 7.1.0 и 7.1.1 выпустила Network Associates. На сайте PGP.COM, которой NA недавно продала всю линейку продуктов PGP, и которая уже анонсировала новую версию (см. http://www.bugtraq.ru/rsn/ archive/2002/08/18.html) 8.0 пока тихо. Источник: CNet [http:// news.com.com/2100-1001956815.html?tag=cd_mh ]

Не доверяй никому Обнаруженная недавно проблема с ошибочной реализацией SSL в браузерах Internet Explorer и Konqueror (последний, впрочем, уже исправился) в равной мере относится и к другому продукту, использующему тот же engine - MS Outlook. Отсутствие каких-либо предупреждений о несоответствии сертификата (который, в принципе, валидный, но принадлежит другому), позволяет относительно легко подделывать цифровые подписи под сообщениями. Если Microsoft в ближайшее время серьезно не займется этой проблемой, следует ожидать взрыва злоупотреблений на этой почве, ведь с использованием этой уязвимости уже написана специальная программа - sslsniff, которая живет по адресу http:// www.thoughtcrime.org/ie.html. http://www.theregister.co.uk/ content/55/26924.html ]

Обновлен стандарт безопасности Впервые за последние 10 лет был подвергнут серьезному пересмотру стандарт безопасности, разработанный Организацией по Экономическому Сотрудничеству и Развитию (Organisation for Economic Cooperation and Development, OECD). В новом варианте 7799 много внимания уделено вопросам обучения и изначальной интеграции процедур безопасности в информационные системы, а также оценке рисков. Jeremy Ward, представитель Confederation of British Industry (CBI) назвал новый документ «зеленым светом на информационной супермагистрали». Что же, надо почитать, чего там такого революционного. Источник: vnunet [ http:// www.vnunet.com/News/1134825 ]

овая программа: mod_samoylyk - модуль для динамического конфигурирования виртуальных хостов Модуль для переадресации виртуальных хостов с возможностью установки uid и gid пользователя для suexec (примерно cgiwrap + suexec = mod_samoylyk). Описание пользовательских хостов находятся в независимой базе, при добавлении или изменении виртуального хоста перезапуск apache не требуется, каждый виртуальный хост использует примерно 0,3 кб памяти (VirtualHost в apache сьедает 10,5 кб) Качать отсюда http:// www.samoylyk.com/modules/ mod_samoylyk.tar.gz

Microsoft латает дырку в SSL Linux на SmatrPhone Linux портирован на SmatrPhone китайской компанией CMS. Причем это вполне законченный коммерческий продукт. http://linuxworld.com.au/ news.php3?nid=1797&tid=1nY

4

Вышел бюллетень по недавно обнаруженной уязвимости в реализации SSL и патч, исправляющий ее. Источник: Microsoft [http:// www.microsoft.com/technet/treeview/ default.asp?url=/technet/security/ bulletin/MS02-050.asp ]

Украина переходит на свободные программы 15 августа на рассмотрение Верховной Рады Украины подан проект Закона «Об использовании Открытого (Свободного) программного обеспечения в государственных учреждениях и в государственном секторе хозяйствования». Все госучреждения и учреждения и предприятия государственного сектора народного хозяйства должны использовать только «открытое» ПО, под которым имеются в виду программы c открытым исходным кодом, распространяемые по лицензии GPL, а также по лицензиям типа Apache или BSD. Использование коммерческого программного обеспечения допускается только в исключительных случаях и в порядке, изложенном в ст. 6 законопроекта. При этом, одним из обязательных требований является способнос ть данного программного обеспечения сохранять информацию в свободных (открытых) форматах. Забавно, но при этом сам текст законопроек та [ht tp:// oracle2.rada.gov.ua/pls/zweb/ webproc34?id=&pf3511= 12883&pf35401=23268 ] набран в MS Word. Источник: Верховная Рада Украины [http://oracle2.rada.gov.ua/ pls/zweb/webproc34?id=&pf3511= 12883&pf35401=23268 ]

Новая программа: ogle DVD проигрыватель (GPL) для Solaris, Linux и BSD Проигрыватель DVD видеодисков с поддержкой DVD-меню, позволяет проигрывать криптованые DVD диски, возможность производить скриншоты, реализована возможность поиска. Поддерживаемые форматы: AC-3, MPEG[2], LPCM. http://www.dtek.chalmers.se/ groups/dvd/


тенденции Network Associates купила разработчика «трояна» DragNet После улаживания финансового вопроса с акционерами McAfee.com по возвращению контроля, калифорнийская компания сетевой безопасности Network Associates заявила о приобретении Traxess Inc. из штата Юта. Основной продукт Traxess — «DragNet» - программа, которая отсылает и воспроизводит нажатия клавиш с одного компьютера на другой. Идея программы-шпиона состоит в контроле действий сотрудников со стороны службы безопасности компании. Программа предназначена для работы в гигабитных сетях и использует запатентованные технологии, позволяющие «шпиону» загружать вторую копию файлов вплоть до больших MP3-файлов или потокового видео, чтобы можно было увидеть, как именно неправильно сотрудник использует рабочий компьютер. Председатель Traxess говорит, что он рад передать продукт, создавашийся годами, в такие надежные и, главное, популярные руки, как Network Associates. Network Associates тоже преуспела в создании аналогичных программ, среди которых - платформа Sniffer («Нюхач» — термин, обозначающий программу перехвата трафика), а точнее — Архитектура Управления Корпоративным Сниффером — для работы с любыми видами трафика в высокоскоростных сетях.

Установка и настройка Linux на ноутбуке В руководстве по установке Red Hat 7.3 на ноутбуке Compaq Presario 711CL затрагиваются такие моменты как настройка ACPI подсистемы управления питанием, мониторинг уровня заряда батарей, настройка звука и подключение USB устройств. ht tp://www.linuxjournal.com/ article.php?sid=6291&mode=thread&order=0Ao

№1, октябрь 2002

Microsoft IIS6: вебсервер строгого режима Проектируя IIS6, разработчики из Microsoft действовали самоотверженно. Философией софтверного гиганта всегда было создание продуктов, максимально доступных, максимально «скриптабельных» и максимально мощных. Теперь все не так. После двух лет нападок со стороны секьюрити- консультантов и интернет-вандалов разработчики Microsoft решили, что, когда дело касается интернета, лучшая часть отваги — это осторожность. Теперь они думают лишь о том, как помешать пользователям добраться до разных функций.

Новая программа: ScanDoc - построение документации через сканирование C++ исходников Программа позволяет просканировать исходники на C++ и построить основываясь на комментариях внутри исходников проиндексированную документацию для функций и блоков сканируемой программы. http://scandoc.sourceforge.net/

Регулярные выражения в Perl 6 Продолжение серии статей, посвященных технологиям Perl 6, цель данной статьи - знакомство с новыми возможностями построения регулярных выражений (regex). http://www.perl.com/pub/a/2002/08/ 22/exegesis5.html

MPlayer 0.90-pre7 Удален старый код вроде libvo2, подчищен libmpcodecs, добавлена поддержка аудио-кодека sipr и поддержка realvideo, сделаны новые фильтры, переписан видеозахват и многое другое. ht tp://www.mplayerhq.hu/ homepage

Опасные игры Закон, принятый правительством Греции, запрещаетэлектронные игры — даже на мобильном телефоне — под страхом внушительного штрафа или длительного тюремного заключения. Можно попасть в тюрьму за убийство... монстра в видеоигре. Источник: Zdnet.ru [ http:// zdnet.ru/?ID=287110 ]

Wine Tools Wine Tools 1.13 Вышла новая версия Wine Tools — Wine Tools 1.13. Данная программа (или скорее набор программ) умеет: — Инсталлировать приложения. — Деинсталлировать их. — Создавать виртуальный Windows диск. — Редактировать конфигурационный файл Wine. http://www.franksworld.net/wine/ winetools/s

Linux постепенно отвоевывает позиции ... Если верить статье, то все больше IT специалистов подумывают о переводе серверов на Linux. 31 процент перевел сервера с Windows, 24 — c других Unix и 14 — с других операционных систем. Правда, 46 процентов пока не владеют серверами на основе Linux или не собираются переводить — так что есть куда расти. http://news.com.com/2100-1001956496.html?tag=fd_topM

Ряд статей для людей, использующих ОС AIX Журнал SysAdmin Magazine открыл online доступ к некоторым интересным статьям по OC AIX опубликованным в 2001 году. http://www.samag.com/articles/ 2001/0106/supplement/

5


FAQ PERL

Чем отличается синтаксис скриптов на UNIX и WIN платформах? Юниксовые скрипты плохо воспринимает досовский перевод строки — CR LF. Если открыть такой файл в vi в конце строк будут ^M. Удалить их можно, например таким скриптом: #!/bin/bash install -d -m 0775 orig cp $1 orig/$1.orig. date +%m-%d-%H.%M sed -e «s/^M//g» $1 >oooo mv -f oooo $1

Как сделать так, чтобы скрипт работал в фоновом режиме, как демон? Варианта два. Первый - воспользоваться модулем Proc::Daemon, второй — сделать все самому, примерно так: use strict; require sys/syscall.ph ; # Óñòàíàâëèâàåì ïóòü ïî óìîë÷àíèþ $ENV{PATH} = /bin:/usr/bin ; # ×èñòî äëÿ ïðèêîëà $0= mydaemon ;

Как мне получить зашифрованный пароль? Стандартной функцией crypt(), например вот так: # Calculate salt value for crypt function @saltair= split // ,»ABCDEFGHIJKLMNOPQRSTUVWXYZ abcedfghijklmnopqrstuvwxyz0123456789.»; sub get_salt { srand(); $iOff = int(rand($#saltair)); $iOff2 = int(rand($#saltair)); return join(«»,$saltair[$iOff], $saltair[$iOff2]); } $crypted_passwd = crypt($plain_passwd,get_salt());

А как мне проверить соответствие введенного пароля зашифрованному? Первых два символа пароля соджержат шифровальный ключ. Если, взяв их, зашифровать проверяемый пароль, то зашифрованные строки должны совпасть. Пример: if (crypt($entered_passwd, subst($crypted_passwd,0,2)) eq $crypted_passwd) { # Ïàðîëü âåðåí } else { # Ïàðîëü íå âåðåí }

6

# Îòäåëÿåìñÿ îò ðîäèòåëÿ fork() && exit; # Îòêëþ÷àåìñÿ îò òåðìèíàëà close STDOUT; close STDERR; close STDIN; # Äåëàåì êîðåíü òåêóæèì êàòàëîãîì chdir / ; # Ñîçäàåì íîâóþ ñåññèþ è ñòàíîâèìñÿ ëèäåðîì # ãðóïïû ïðîöåññîâ, ÷òîá íàñ ñëó÷àéíî íå ïðèáèëè syscall(&SYS_setsid); # Ïåðåõâàòûâàåì ñèãíàëû, äëÿ êîððåêòíîãî âûõîäà $SIG{ INT } = $SIG{ QUIT } = $SIG{ TERM } = quit ; $SIG{ HUP } = ignore ;

Как сделать так, чтобы программа гарантированно работала только в одном экземпляре? Способ первый, принятый в мире Unix: $pidfile = /var/run/mydaemon.pid ; if (-e $pidfile) { # aha, pid file is here, but process could be dead by now my $myfile=file_name($0); unless (open(PIDFILE,$pidfile)) { # too dangerous to start because I can t read old PID exit 1; } my $oldpid=<PIDFILE>; close PIDFILE; # see if there is a process with such pid if ($oldpid > 1 && kill(0,$oldpid)) { # another proccess is running already exit 1; } else { # that process is long dead } } # write pid file open (PID, «>$pidfile») or die; print PID $$; close(PID);

# Äåëàåì íàøè òåìíûå äåëà ...

# do some work ...

# Âûõîäèì quit();

# remove pid file unlink $pidfile; exit(0);

sub quit { # Ïîìåùàåì ñþäà êîä äëÿ êîððåêòíîãî # ïðåêðàùåíèÿ ðàáîòû ... exit(0); }

Если Вы хотите написать демона, реализующего работу через сеть, рекомендуем ознакомиться с модулем Net::Daemon.

Как защитить мою программу, чтобы никто не смог её прочитать? Perl-сценарий представляет собой открыто распространяемый код.

Способ второй, основанный на блокировании файлов: # make a lock $lockfilename=»/tmp/mydaemon.lock»; unless (open (LOCKFILE,»>$lockfilename»)) { die «cannot open lock file\n»; } unless (flock (LOCKFILE,LOCK_EX|LOCK_NB)){ print «my copy is already running\n»; exit(0); } # do some work ... # unlock lock file close(LOCKFILE); unlink($lockfilename);

по материалам www.xpoint.ru составил Дмитрий Горяинов


АДМИНИСТРИРОВАНИЕ


администрирование

ХЕВИ ХАРДВЭР Интеллектуальные свитчи (порусски коммутаторы) Cisco Catalyst серий 2900XL и 3500 предназначены для крупных корпоративных сетей. Они представляют собой коммутаторы высокого класса с микропроцессорным управлением, флэш-памятью, объёмом 4 Мб и DRAM-памятью объёмом 8мб. На данных устройствах обычно установлена специализированная операционная система Cisco IOS.

УСТАНОВКА И НАСТРОЙКА КОММУТАТОРОВ CISCO CATALYST СЕРИЙ 2900XL И 3500 ВСЕВОЛОД СТАХОВ В данной статье я буду преимущественно говорить о версии 12.0.x. (отличия версий, в основном, в вебинтерфейсе и поддержке тех или иных технологий). На каждый из коммутаторов может быть установлено программное обеспечение стандартного (Standard Edition) и расширенного типа (Enterprise Edition). В enterprise edition входят: поддержка магистралей 802.1Q, протокол TACACS+ для единой авторизации на свитчах, модифицированная технология ускоренного выбора Spanning Tree (Cisco Uplink Fast) и др. Здесь я преимущественно буду описывать настройку свитчей с помощью интерфейса командной строки (CLI). Данные свитчи предоставляют множество сервисных возможностей. Кроме этого, они идеально подходят для крупных сетей, так как имеют высокую пропускную способность — до 3-х миллионов пакетов в секунду, большие таблицы адре-

8

сов (ARP cache) — 2048 mac адресов для Catalyst 2900XL и 8192 для Catalyst 3500, поддерживают кластеризацию и виртуальные сети (VLAN), предоставляют аппаратную безопасность портов (к порту может быть подключено только устройство с определённым mac адресом), поддерживают протокол SNMP для управления, используют удалённое управление через веб-интерфейс и через командную строку (т.е. через telnet или модемный порт). Кроме этого, имеется возможность мониторинга портов, т.е. трафик с одного порта (или портов) отслеживается на другом. Многим покажется полезной возможность ограничивать широковещательный трафик на портах, предотвращая тем самым чрезмерную загрузку сети подобными пакетами. Исходя из всего этого, можно утверж дать, что выбор свитчей Cisco Catalyst является идеальным для крупных и средних сетей, так как несмотря на высокую стоимость

(>1500$), они предлагают широкий выбор сервисных функций и обеспечивают хорошую пропускную способность. Наиболее привлекательными возможностями данных свитчей являются: организация виртуальных сетей (в дальнейшем VLAN), полностью изолированных друг от друга, но синхронизированных между свитчами в сети, и возможность кластеризации для единого входа в систему управления свитчами и наглядного изображения топологии сети (для веб-интерфейса). Перспективным является использование многопортового свитча в качестве центрального элемента сети (в звездообразной архитектуре). Хотя свитчи поставляются с подробной документацией, но она вся на английском языке и нередко не сообщает некоторых вещей, а иногда, напротив, бывает слишком избыточной. Для начала хотел бы рассказать о первоначальной настройке свитча. Итак, Quick Start.


администрирование Присоединение консольного кабеля n Подключите поставляемый плоский провод в разъём на задней панели коммутатора с маркой console. n Подключите другой конец кабеля к com-порту компьютера через соответствующий переходник и запустите программу-эмулятор терминала (например, HyperTerminal или ZOC). Порт консоли имеет следующие характеристики: n а) 9600 бод; n b) Нет чётности; n c) 8 бит данных; n d) 1 бит остановки. Важное замечание для кластера (объединения нескольких коммутаторов): если вы хотите использовать коммутатор в качестве члена кластера, то можно не присваивать ему IP адрес и не запускать построитель кластера. В случае командного свитча, вам необходимо выполнить следующий пункт. n Присвоение IP коммутатору В первый раз, когда вы запускаете свитч, то он запрашивает IP адрес. Если вы назначаете ему оный, что весьма желательно, то он может конфигурироваться через Telnet.

Необходимые требования к IP Перед установкой необходимо знать следующую информацию о сети: n IP адрес свитча. n Маска подсети. n Шлюз по умолчанию (его может и не быть). n Ну и пароль для свитча (хотя, скорее всего лучше это придумать самому).

Первый запуск Выполняйте следующие действия для присвоения коммутатору IP адреса: Шаг 1. Нажмите Y при первой подсказке системы: Continue with configuration dialog? [yes/no]: y

№1, октябрь 2002

Шаг 2. Введите IP адрес: Enter IP address:

Шаг 3. Введите маску подсети и нажмите Enter: Enter IP netmask:

Шаг 4. Введите, есть ли у вас шлюз по умолчанию N/Y, если есть, то наберите его IP адрес после нажатия Y: Would you like to enter a default gateway address? [yes]: y

Шаг 5. Введите IP адрес шлюза: IP address of the default gateway:

Шаг 6. Введите имя хоста коммутатора: Enter a host name:

Шаг 7. Введите пароль. Кроме этого, затем на вопрос о пароле для Telnet ответьте Y и введите пароль для доступа через Telnet, так как иначе возможны странности работы с telnet. У меня, к примеру, подключение Telnet к свитчу обрывалось по причине: пароль нужен, но не определён: Enter enable secret:

Создался следующий файл конфигурации: Initial configuration: interface VLAN1 ip address 172.16.01.24 255.255.0.0 ip default-gateway 172.16.01.01 enable secret 5 $1$M3pS$cXtAlkyR3/ 6Cn8/ snmp community private rw snmp community public ro end Use this configuration? [yes/no]:

Шаг 8. Если всё нормально - жмите Y; нет - N (только учтите, что пароль хранится в зашифрованном виде).

рировать его через веб-интерфейс с помощью Cisco Visual Switch или через консоль (Telnet или модемный порт). Выбор средства настройки — ваш выбор, но учтите, что веб-интерфейс обладает сильной «тормознутостью», так как основан целиком на апплетах Java (не забудьте включить поддержку Java в браузере). Кроме этого Cisco Visual Switch работает только в браузерах Microsoft IE и Netscape (хотя у меня в Netscape 6.0 ничего не работало). К достоинствам этого типа настройки можно отнести наглядность, простоту и возможность получить помощь по всем пунктам. Интерфейс командной строки является немного сложным для тех, кто редко работает с консолью, но настройка через командную строку является очень быстрой и предоставляет дополнительные возможности. Альтернативным способом настройки является веб-консоль. В ней показываются в виде гиперссылок допустимые команды CLI, и вы можете собрать нужную последовательность команд как бы из кирпичиков. Далее, можно настроить VLAN, вообще термин VLAN — виртуальная локальная сеть. Такая сеть отличается от физической LAN лишь тем, что организуется разделение пакетов в единой локальной сети так, как если бы это были разные подсети. Таким образом, с помощью VLAN можно организовать деление локальной сети на отдельные участки. При этом существует возможность регулировать взаимодействие VLAN весьма широко.

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

Открытие Cisco Visual Switch Manager Software После того, как вы присвоили IP коммутатору, то вы можете конфигу-

9


администрирование DEV_PLUS_VID_NO_PAD(ïî óìîë÷àíèþ) èìÿ óñòðîéñòâà áóäåò âûãëÿäåòü òàê: eth0.5

Включаем интерфейс физической сетевой карты, но без IP адреса (в документации сказано, что если у реальной сетевой карты есть IP адрес, то vlan работать не будут, но у меня это прошло без особых проблем, главное, чтобы все сетевые устройсва были в РАЗНЫХ ПОДСЕТЯХ, причём это касается и виртуальных LAN, иначе вообще ничего работать не будет — проверял): # ifconfig eth0 0.0.0.0 up

Каждый VLAN добавим на нужный интерфейс (не добавляйте VLAN по умолчанию, так как пакеты этого VLAN идут без инкапсуляции): # vconfig add eth0 2 # vconfig add eth0 3

VLAN (при этом необязательно, чтобы они были на том же свитче). При этом абсолютно исключается возможность взаимодействия с «чужим» портом. А сами коммутаторы соединяются между собой посредством особых каналов связи - trunk магистралей. По таким магистралям проходят данные всех VLAN. Но, к сожалению, trunk порт должен быть point-to-point (о двух концах) и может подключаться только к свитчам и роутерам, поддерживающим VLAN, так как к пакету добавляется 4-х байтный тег, содержащий информацию о VLAN и её приоритете. Таким образом, организация сетевого доменного сервера становится возможной только при использовании роутера. Но сейчас есть выход в использовании ОС Linux, которая поддерживает данный протокол на уровне драйвера ядра, VLAN фигурируют в качестве виртуальных сетевых устройств. Здесь нужно только указать правильный тип инкапсуляции пакетов - IEEE 802.1q (по умолчанию используется тип ISL). При этом, если все VLAN находятся в одной подсети, то маршрутизация будет проходить только на уровне статических путей. Данной теме посвящено достаточное количество документации, и пакет

10

vlan-utils присутствует во многих современных дистрибутивах Linux (Mandrake и ALT Linux). С ядра 2.4.9 опция компиляции CONFIGURE_802Q присутствует на странице NET как экспериментальная, поэтому вы должны поставить соответствующую опцию CONFIGURE_EXPERIMENTAL(в ядрах 2.5 эта опция уже не экспериментальная), затем make dep —> make bzImage. Настройка VLAN со стороны Linux тоже не должна вызывать трудностей. Для настройки служит утилита vconfig из пакета vlanutils. Для получения этой утилиты, а также патчей для ядра откройте http:/ /scry.wanfear.com/~greear/vlan.html. Патч для ядра описан в FAQ на данной странице, я же ограничусь примером настройки 2 VLAN на Linux машине: vconfig set_name_type [name-type] òèï íàèìåíîâàíèÿ âèðòóàëüíûõ óñòðîéñòâ, ìîæåò ïðèíèìàòü ñëåäóþùèå çíà÷åíèÿ: VLAN_PLUS_VID èìÿ óñòðîéñòâà áóäåò âûãëÿäåòü òàê: vlan0005 VLAN_PLUS_VID_NO_PAD èìÿ óñòðîéñòâà áóäåò âûãëÿäåòü òàê: vlan5 DEV_PLUS_VID èìÿ óñòðîéñòâà áóäåò âûãëÿäåòü òàê: eth0.0005

Всё, мы прописали VLAN’ы, которые будут использоваться на интерфейсе, нужно присваивать им Ip адреса (тип имени см. выше) в разных подсетях: # ifconfig -a # ifconfig -i vlan0002 192.168.2.1 broadcast 192.168.2.255 netmask 255.255.255.0 up # ifconfig -i vlan0003 192.168.3.1 broadcast 192.168.3.255 netmask 255.255.255.0 up

Технические параметры Производительность Êîììóòàöèîííàÿ øèíà 8.8 Ãáèò/ñåê Ñêîðîñòü êîììóòàöèè 64-áàéòîâûõ ïàêåòîâ Catalyst 2950-12: 1.8 Ìïàêåò/ñåê Catalyst 2950-24: 3.6 Ìïàêåò/ñåê Catalyst 2950T-24: 6.6 Ìïàêåò/ñåê Catalyst 2950C-24: 3.9 Ìïàêåò/ñåê Ìàêñèìàëüíàÿ ñêîðîñòü êîììóòàöèè 4.4 Ãáèò/ñåê 8 Máàéòíàÿ ðàñïðåäåëåííàÿ àðõèòåêòóðà ïàìÿòè äëÿ âñåõ ïîðòîâ 16 Máàéò DRAM è 8 MÁàéò ôëýø ïàìÿòü Ïîääåðæêà 8,192 MAC àäðåñîâ Ñòàíäàðòû IEEE 802.1x support (planned future software support) IEEE 802.3x full duplex on 10BaseT, 100BaseTX, and 1000BaseT ports IEEE 802.1D Spanning-Tree Protocol IEEE 802.1p CoS IEEE 802.1Q VLAN IEEE 802.3ab 1000BaseT specification IEEE 802.3u 100BaseTX specification IEEE 802.3 10BaseT specification Äàííûå âçÿòû ñ ñàéòà amt.ru


администрирование Если вы планируете использовать маршрутизацию в сети, то добавьте следующее: # echo 1 > /proc/sys/net/ipv4/ ip_forward

Для удаления VLAN используйте синтаксис: # vconfig rem vlan0002

Подобные скрипты удобно прописать в файле /etc/init.d/network. Кстати, учтите, что MTU в trunk магистралях не 1500, а 1504 из-за того самого тега. Trunk магистрали поддерживаются всеми типами свитчей, которые умеют делать VLAN. Другое преимущество использование особого протокола CISCO, обеспечивающего централизованное управление всей системой VLAN - VTP (vlan trunk protokol). Например, вы можете на сервере VTP отключить или включить определённую VLAN. Широковещательный трафик по умолчанию не распространяется между VLAN. MultiVLAN очень интересный тип организации VLAN. Он состоит в определении для порта нескольких допустимых VLAN (например, для экономистов это могут быть VLAN Economics и Server для доступа к общим серверам и.т.д.). Такой тип подходит для организации сети с помощью одного центрального Catalyst свитча и нескольких других свитчей или хабов, к нему подключённых. Общий сервер также должен подключаться к центральному Catalyst свитчу к multiVLAN порту для общения со всеми рабочими станциями. Широковещательные же пакеты будут распространяться по всей сети (разумно включить фильтры шторма). Но при выборе типа VLAN, учтите, что на одном свитче не может быть разных типов организации VLAN, из-за особенности протоколов (невозможна синхронизация внутреннего формата VLAN пакетов и trunk магистралей, т.к. в последних можно указывать лишь один номер VLAN). Ни в коем случае не соединяйте свитчи Catalyst multiVLAN портами, т.к. это приведёт к тому, что другой свитч, получивший пакет с multiVLAN порта, не сможет определить к какому VLAN он относится и присвоит ему VLAN по умолчанию (1 VLAN).

№1, октябрь 2002

Итак, вы подсоединили нуль-модемный кабель или запустили сеанс telnet. Во-первых, на приглашение HOST_NAME> надо ответить enable и ввести пароль к свитчу для получения доступа к конфигурации. Для просмотра сведений о свитче наберите show running-config. Подсказка по интерфейсу CLI. Здесь есть такие удобства как автодополнение кнопкой TAB — наберите начало команды, например show ru<TAB>, и оно расширится в show running-config. Можно в любой момент получить справку по любому вопросу: просто нажмите ? и вам будут предложены возможные параметры команды, например, show ?. Для повторения предыдущих или следующих команд можно использовать курсоры вверх или вниз. Для пролистывания текста при запросе —more— нажимайте пробел для опускания текста вниз на строк у. Итак, вы набрали show running-config, после этого отобразится информация о текущих настройках. Вначале общая информация о свитче (адрес, имя, адреса шлюз и.т.д.), а затем информация о портах. Здесь особое внимание я бы хотел обратить на информацию о режиме VLAN порта: switchport mode %%%%. Access - ðåæèì ñòàòè÷åñêîé VLAN; Multi - ìóëüòèVLAN ïîðò; Trunk - ðåæèì trunk ìàãèñòðàëè; Dynamic - VPMS ðåæèì.

Следующий параметр switchport показывает особые параметры для данного типа порта. Например для access это единственный идентификатор VLAN, для multi - список допустимых VLAN, разделённых «,» или «—» для указания промежутка VLAN. Таким об-

разом, после сделанных изменений неплохо было бы смотреть, что именно произошло. Для постоянного сохранения параметров настройки наберите write memory. Для перезагрузки свитча используйте команду reload. И заканчивая эту тему, подскажу, как сбросить настройки свитча после неудачных опытов: >enable #rename flash:config.text flash:ÄÐÓÃÎÅ_ÈÌß.text #reload

Если вы забыли пароль, то дело ещё хуже. Для смены пароля нужно: n Выключить питание свитча. n Подсоединить нуль-модемный кабель. n При включении питания удерживать кнопку mode. n При появившейся подсказке набрать flash_init (инициализация файловой системы). n rename flash:config.text flash: ДРУГОЕ_ИМЯ.text n Перезагрузите свитч. Кстати, по опыту знаю, что всегда надо писать flash:, хотя где-то имена файлов принято искать вначале на flash:, но это не везде срабатывает. Однажды я попытался обновить операционную систему, сгрузил файл ядра и написал boot имя_файла (забыл flash:) - последствия были печальными: свитч отказывался найти ядро и не загружался, а так как он был в труднодоступном месте, то всё было ещё печальнее. Вы сразу же приступите к начальному конфигурационному диалогу. Для выхода из вложенных режимов

11


администрирование конфигурации нажимайте exit. Для полного выхода наберите два раза exit. Итак, приступим к настройке VLAN. Предполагаю, что вы уже находитесь в режиме конфигурации: (config)# interface FastEthernet x/x (íóæíûé âàì ïîðò), çàòåì âû â ðåæèìå êîíôèãóðàöèè ïîðòà - (config-if)#

Теперь вам доступны любые доступные изменения конфигурации порта. Наименование порта происходит по следующей схеме: n Тип порта (FastEthernet (100 Мб), Ethernet (10 Мб), Gigabit (1 Гб)). n Номер модуля (0 для встроенных портов и далее 1, 2, 3 для дополнительных модулей). n Номер порта в модуле. Для получения списка допустимых команд, как обычно, можно нажать ?. Подробнее остановлюсь на команде switchport mode, определяющей режим работы порта для VLAN. Допустимые значения access (статический доступ), multi (мультидоступ) и trunk (режим туннельной магистрали). Для конфигурации конкретного режима нужно применять: switchport access vlan ID - åäèíñòâåííàÿ vlan switchport multi vlan ID, ID, ID èëè switchport multi vlan ID-ID, ID-ID, ID - ñïèñîê äîïóñòèìûõ vlan switchport trunk allowed vlan LISTID - äîïóñòèìûå äëÿ ìàãèñòðàëè vlan(ïî óìîë÷àíèþ 1-1005) prunning vlan LISTID - ôèëüòðàöèÿ øèðîêîâåùàòåëüíîãî òðàôèêà ìåæäó vlan encapsulation - òèï ïàêåòà(disl èëè dot1q - IEEE 802.1q) native vlan ID - äëÿ trunk ìàãèñòðàëè òèïà IEEE 802.1q íîìåð vlan, äëÿ êîòîðîé íå èçìåíÿåòñÿ ôîðìàò ïàêåòà (ïî óìîë÷àíèþ 1 vlan)

Для отмены каких-либо значений воспользуйтесь командой no switchport ... и применяйте те же команды, что и для включения опций, но применяйте их в обратном порядке, то есть: no switchport multi vlan ... no switchport mode multi

Не забудьте посмотреть результаты вашей работы: (config-if)#exit (config)#exit #show running-config #write memory - åñëè íàäî çàïèñàòü íàñòðîéêè

12

Для осмысленной настройки VLAN можно использовать базу данных VLAN: #vlan database (vlan)# (vlan)#show - äëÿ ïîêàçà ñîñòîÿíèÿ vlan íà äàííîì ñâèò÷å:

vtp password password-value

n Установите нужный режим VTP для данного свитча (клиент/сервер). vtp server(client)

Далее можно поменять настройки конкретной VLAN: (vlan)#vlan ID ? - ñïèñîê âîçìîæíûõ íàñòðîåê:

Например, name - настройка имени для данной vlan. Довольно удобно давать VLAN осмысленные имена, но нужно иметь в виду, что если на разных свитчах одни и те же vlan будут иметь разные имена, то это может вызвать путаницу в дальнейшем обслуживании. VTP — протокол trunk магистралей — позволяет централизовать управление vlan с сервера VTP. Информация от сервера распространяется к клиентам через trunk магистрали. В ней содержится полная информация о VLAN, сконфигурированных на сервере. Клиент, в соответствии с этой конфигурацией, выполняет синхронизацию своей конфигурации. Для настройки VTP в режим сервера выполните следующее: n В основном режиме войдите в раздел конфигурации VLAN. vlan database

n Введите имя домена VTP(1 — 32 символов). vtp domain domain-name

n (Необязательно) Установите пароль для домена(1-64 символа).

n Возвращаемся в основной режим. exit

n Проверяем настройки VTP. show vtp status

Пример настройки VTP сервера: # vlan database (vlan)# vtp domain Avitek Óñòàíîâêà èìåíè äîìåíà Avitek (vlan)# vtp domain Avitek password LAVA Óñòàíîâêà ïàðîëÿ äëÿ äàííîãî äîìåíà. (vlan)# vtp server Âêëþ÷åíèå ðåæèìà VTP ñåðâåðà. (vlan)# exit Íàñòðîéêè ïðèìåíåíû. Âûõîäèì.... # show vtp status VTP Version Configuration Revision Maximum VLANs supported locally Number of existing VLANs

: : : :

2 0 68 6

Семейства коммутаторов Cisco Catalyst 2900XL/3500XL Семейство коммутаторов 2-го уровня Catalyst 2900XL представлено пятью различными моделями: 1) WS-C2912-XL — содержит 12 универсальных портов 10/100 Mbps Ethernet с автоматическим определением скорости и режима передачи; 2) WS-C2924-XL — содержит 24


администрирование универсальных порта 10/100 Mbps Ethernet с автоматическим определением скорости и режима передачи; 3) WS-C2924M-XL — содержит 24 универсальных порта 10/100 Mbps Ethernet с автоматическим определением скорости и режима передачи, а также два слота для установки дополнительных интерфейсных карт; 4) WS-C2924С-XL — содержит 22 универсальных порта 10/100 Mbps Ethernet с автоматическим определением скорости и режима передачи, а также два оптических порта 100 Mbps Fast Ethernet; 5) WS-C2912MF-XL — содержит 12 оптических портов 100 Mbps Ethernet, а также два слота для установки дополнительных интерфейсных карт. В состав семейства коммутаторов Catalyst 3500XL входит три модели: 1) WS-C3512-XL — содержит 12 универсальных портов 10/100 Mbps Ethernet с автоматическим определением скорости и режима передачи, а также два порта Gigabit Ethernet; 2) WS-C3524-XL — содержит 24 универсальных порта 10/100 Mbps Ethernet с автоматическим определением скорости и режима передачи, а также два порта Gigabit Ethernet; 3) WS-C3508G-XL — содержит 8 портов Gigabit Ethernet. Комму таторы этих семейс тв предназначены для работы в качестве сетевого оборудования рабочих групп среднего и малого размера и имеют для своего класса очень высокую производительность — до 3-х миллионов пакетов в секунду, которая обеспечивается мощным модулем коммутации архитектурой коммутатора, использующей разделяемую память, беспрецендентные механизмы управления и контроля за работой устройства и т.п. Высокая производительнос ть серии 2900XL подтверждена в серии испытаний таких тестовых лабораторий как Mier, ZDnet и др.

№1, октябрь 2002

Коммутаторы семейств 2900XL, 3500XL, а также Catalyst 1900/2820 могут объединяться в стеки (до 16 устройств) при помощи соединений Fast Ethernet, Fast EtherChannel (аггрегирование Fast Ethernet по 2 или 4 канала), а также Gigabit Ethernet и Gigabit EtherChannel. Максимальное количество портов, которое может быть установлено в одном стеке равно 380. Такой стек является единым объектом сетевого управления, которое может выполняться как при помощи командного языка CLI с консоли или при помощи протокола telnet, так и при помощи специализированных систем управления типа CWSI (Cisco Works for Switched Internetworks), так и при помощи WEB-технологии c любой рабочей станции, оснащенной программами просмотра Netscape или Internet Explorer. Как и все коммутаторы, входящие в семейство Catalyst устройства 2900/3500 обеспечивают построение виртуальных сетей (в варианте программного обеспечения Enterprise), режим безопасности, при котором к коммутатору могут быть подключены только станции с указанными MAC-адресами, 4 группы RMON, специальный порт для контроля трафика, проходящего через группу портов или в заданном VLANе и др. Модульные модели коммутаторов (WS-C2924M-XL, WS-C2912FMXL) позволяют устанавливать дополнительные 4-х портовые модули 10/ 100 Mbps Ethernet (витая пара), 2-х и 4-х портовые 100 Mbps Ethernet (оптика мультимод), однопортовые модули Gigabit Ethernet (мультимод, мономод), а также однопортовые модули ATM 155 Mbps (витая пара, мультимод, мономод). Мономодовые модули для ATM и Gigabit Ethernet могут быть выполнены в различных вариантах дальности, при этом предельная длина оптических каналов может достигать 70 км. Трансиверы Gigabit Ethernet (GBIC — Gigabit Interface Converter), устанавливаемые в комму таторах семейства Catalyst 3500XL, а также в соответствующих модулях для коммутаторов семейства Catalyst 2900XL являются сменными, что позволяет гиб-

ко и эффективно конфигурировать коммутаторы для подключения к каналам требуемого типа. На каждый из коммутаторов может быть установлено программное обеспечение стандартного (Standard Edition) и расширенного типа (Enterprise Edition). Расширенная редакция дополнительно поддерживает транкинг (ISL/802.1Q), протокол TACACS+ для регламентации доступа к коммутаторам, модифицированную технологию ускоренного выбора Spanning Tree (Cisco Uplink Fast) и др.

Технические спецификации Производительность: 1) 3.2 Gbps — коммутирующих модуль (для моделей Catalyst 2900XL); 2) 10.0 Gbps — коммутирующих модуль (для моделей Catalyst 3500XL); 3) 3.0 million-pps пропускная способность (64-х байтовые пакеты, Catalyst 2900XL); 4) 7.5 million-pps пропускная способность (64-х байтовые пакеты, Catalyst 3500XL); 5) 4-MB разделяемая память; 6) 8-MB DRAM and 4 MB Флэш-память; 7) 2048 MAC-адресов (Catalyst 2900XL); 8) 8192 MAC-адресов (Catalyst 3500XL). Управление: SNMP Management Information Base (MIB) II, SNMP MIB extensions, Bridging MIB (RFC 1493). Поддерживаемые стандарты: 1) IEEE 802.3x full duplex; 2) IEEE 802.1D Spanning-Tree Protocol; 3) IEEE 802.1Q VLAN; 4) IEEE 802.3z, IEEE 802.3x; 5) IEEE 802.3u 100BaseTX and 100BaseFX specification; 6) IEEE 802.3 10BaseT specification; 7) IEEE 802.3z, IEEE 802.3x 1000BaseX specification; 8) 1000BaseX (GBIC) — 1000BaseSX, 1000BaseLX/LH, 1000BaseZX.

13


администрирование

УДОБНАЯ ПОЧТОВАЯ СИСТЕМА

ВЯЧЕСЛАВ КАЛОШИН

Итак, задача - завести почтовую систему, в которой вся информация о пользователях, доменах и прочем лежала бы в базе данных. Зачем это нужно? Лично для меня это стало актуально после того, как в поддерживаемых мной системах пользователи начали плодиться как кролики. Заводить на каждого свой аккаунт, смотреть, чтобы они не пересекались и не пользовались чужими доменами и так далее. Наконец, мне все надоело и я решил сделать ЭТО. До сих пор возникает множество вопросов по установке и настройке почтовой системы на основе Postfix, CyrusSASL, MySQL, Courier-IMAP, Dr.Web, SquirrelMail. Если использовать приведенный ниже опус в качестве банальной инструкции, у вас должна получиться легко масштабируемая и управляемая система, которая без проблем — как со стороны админа, так и со стороны железа — спокойно будет тянуть по 5-10 тысяч почтовых пользователей. При этом нет разницы: сколько почтовых доменов заведено в системе, как называются пользователи и так далее. Ибо с системными они никак не коррелируют. Пользователи же получат стандартный набор сервисов: SMTP с аутентификацией, pop и imap сервисы, доступ к почте через браузер, плюс, чистую от вирусов почту. Теперь оговорки. Данный текст написан в расчете на тех, кто уже понимает механизмы, происходящие внутри Linux. Если вы неделю как поставили Linux и желаете с помощью этого документа поставить свой hotmail.com, то я ни за что, как обычно, не отвечаю. Все логи и прочее были взяты с моей рабочей системы. Я ничего не выдумывал и не придумывал — это все работает реально. В качестве базовой системы использовался RedHat 7.2. Но аналогично построенные системы работают и на RedHat6.2, и ASPLinux 7.2. Поэтому не вижу причин, почему бы им не работать

14

на других системах. Все необходимые программы и пакеты вы сможете найти в окрестностях freshmeat.net. Если вы желаете задать мне вопрос, задавайте по мылу: multik@multik.ru, но прежде прочтите хотя бы PostfixFAQ на www.postfix.org — ответы на 90% вопросов, на которые я не отвечаю, есть там. Еще одно условие — сначала обдумайте и прочтите все сообщения системы в /var/log/ messages и /var/log/maillog — обычно там есть исчерпывающая информация, почему не работает что-либо. Для нормальных: Просто мне ОЧЕНЬ надоели письма вида: «А чего оно не работает, когда я сделал все так, как ты написал?». И последнее — специально для тех, кто задается вопросом: «Почему я выбрал postfix? Ведь есть стандарт-де-факто sendmail. Есть еще exim,qmail и куча других почтовых серверов». Отвечаю: — Sendmail я вынужден сразу скинуть со счетов. То, что описывается ниже, sendmail не способен выполнить. Или способен, но с очень большими усилиями администратора системы. Я, почему-то органически не могу смотреть на творение djb. Ну а exim я просто не видел. — Courier-IMAP выбран по другой причине: где-то с полгода назад он оказался единственным IMAP сервером, который смог собраться на моей машине и работать под нагрузкой, не требуя к себе внимания. — Dr.WEB — просто хороший анти-

вирус. Нет такой толпы глюков, на которые бы я периодически не напарывался, глядя на AVP. Тем более, что он даже с тривиальной лицензией спокойно выполняет работу почтового фильтра. Правда, с купленной лицензией будет гораздо спокойнее — вирусы даже в архивах не пройдут. Но — если нет лицензии, то — нет. — SquirrelMail — это просто единственная система Web-based почты, которая не вызывает брезгливой реакции при первом, главное, при последующих взглядах. Плюс, она приемлемо работает с русским языком. Теперь устанавливаем SASL. К сожалению, установить его из RPM не получится. Все, что я видел до этого, собрано либо не так, либо не туда. В общем, неработоспособное. Но вы можете попытать удачу. # tar zxvf cyrus-sasl-1.5.27.tar.gz # cd cyrus-sasl-1.5.27.

Заберите патч для поддержки MySQL и LDAP отсюда: http:// www.surf.org.uk/downloads/sasl-1.5.27ldap-ssl-filter-mysql-patch4.tgz. Распакуйте полученный архив и положите sasl-ldap+mysql.patch в корень дерева исходников SASL. # # # #

patch -b -p1 < sasl-ldap+mysql.patch autoheader autoconf automake -i.

Вот и все, патч установлен. Да-


администрирование вайте сконфигурируем SASL. # ./configure with-mysql=/usr/ include/mysql enable-login.

Соберем и установим его: # make # make install # ln -s /usr/local/lib/sasl /usr/lib/ sasl # echo /usr/lib/sasl >> /etc/ ld.so.conf # ldconfig # cd ..

Устанавливаем методы аутентификации: # cat > /usr/local/lib/sasl/smtpd.conf pwcheck_method: mysql mysql_user: postfix mysql_passwd: postfix mysql_host: localhost mysql_database: mail mysql_table: aliases mysql_uidcol: alias mysql_pwdcol: password ^D

Теперь пришла очередь postfix. Здесь ситуация та же. В принципе по сети бродит много rpm, которые содержат в себе откомпилированный postfix с поддержкой MySQL. Я предпочитаю собрать его сам, и быть «чуть-чуть» уверенным в том, что я знаю, что собрал.

# tar zxvf postfix-1.1.11.tar.gz # cd postfix-1.1.11.

Следующие строчки — это одна команда. Ее следует вводить в один прием. # make -f Makefile.init makefiles «CCARGS=-DHAS_MYSQL -DUSE_SASL_AUTH -I/usr/include/mysql -I/usr/local/include -L /usr/local/lib -lsasl -lmysqlclient»

Собираем и устанавливаем: # # # #

make adduser postfix groupadd postdrop make install

Вот ответы, которые я дал инсталлятору: install _ root: [/] tempdir: [/usr/src/post/postfix1.1.11] /tmp config_directory: [/etc/postfix] daemon_directory: [/usr/libexec/ postfix] command_directory: [/usr/sbin] queue_directory: [/var/spool/postfix] sendmail_path: [/usr/sbin/sendmail] newaliases_path: [/usr/bin/newaliases] mailq_path: [/usr/bin/mailq] mail_owner: [postfix]

Установка и настройка MySQL из исходных текстов Для установки MySQL, необходимо: n иметь gcc выше, чем 2.8.1 (egcc 1.0.2), рекомендуется 2.95.2; n создать директорию для сборки, распаковать в нее mysql-3.23.32.tar.gz/mysql-3.23.37.tar.gz (взять на http://download.sourceforge.net/mirrors/mysql/); n для версии ранее 3.23.34 распаковать туда db3.2.3h.tar.gz (это специальная версия bdb для MySQL) n создать группу mysql; n создать пользователя mysql (в группе mysql) (зачем ему bash?); n ./configure —prefix=/usr/local/mysql — localstatedir=/ usr/local/mysql/data — with-unix-socket-path=путь — with-mysqld-user=mysql — disable-large-files — withlibwrap — without-debug — with-charset=cp1251 — with-extra-charsets=all; n make (70MB/91MB); n если upgrade, то остановить mysql, сохранить базы данных и my.cfg (не забыть потом удалить!); n make install (как root)(16 MB, из них 5МБ - тест): • /usr/local/mysql/lib/mysql — эту директорию указывать для libtool, либо занести в /etc/ld.so.conf. n при первой установке: scripts/mysql_install_db (как root — создание таблиц с правами доступа, дает все

№1, октябрь 2002

n n n

n • • • • • • • • n

setgid_group: [postdrop] manpage_directory: [/usr/local/man] sample_directory: [/etc/postfix] readme_directory: [no]

Ура. Postfix встал. Теперь наша задача его отконфигурировать. # cd /etc/postfix/ # mcedit main.cf

Весто mcedit может быть vi, emacs или любой другой предпочитаемый вами текстовый редактор — это некритично. Редактируем главный файл конфигурации postfix, обращая внимание на следующие строчки: broken_sasl_auth_clients = yes smtpd_sasl_auth_enable = yes transport_maps = mysql:/etc/postfix/ transport.cf virtual_mailbox_base = / virtual_uid_maps = mysql:/etc/postfix/ ids.cf virtual_gid_maps = mysql:/etc/postfix/ gids.cf virtual_mailbox_maps = mysql:/etc/ postfix/aliases.cf virtual_maps = mysql:/etc/postfix/ remote_aliases.cf relay_domains = $transport_maps smtpd_recipient_restrictions = p e r m i t _ m y n e t w o r k s , permit_sasl_authenticated,check_relay_domains disable_vrfy_command = yes

права пользователю root без пароля и позволяет делать все с базами test и test_*, кроме раздачи привилегий); chown -R root:mysql /usr/local/mysql (как root); chown -R mysql /usr/local/mysql/data (и отдельную директорию для mysql.sockets с правами чтения для всех; mysql не нужны права на запись для my.cnf); support-files/mysql.server в /etc/rc.d/init.d для автоматического запуска и дать ему права на исполнение и сделать линк K00mysql из rc0.d и rc6.d на него, S99mysql из rc2.d, rc3.d и rc5.d на него; скопировать my-medium.cnf в /usr/local/mysql/data/ my.cnf и слегка отредактировать [mysqld]: socket=имя-файла под Unix-socket (и в раздел [client] тоже); skip-locking (не блокировать доступ к данным от ДРУГИХ процессов); log-bin #журнал изменений для репликации; log-slow-queries; log-update #журнал изменений; skip-networking #если не нужен доступ по TCP/IP (а еще лучше использовать ssh + port forward); safe-show-database; skip-show-database. тестовый запуск: /usr/local/mysql/bin/safe_mysqld — user=mysql (как root)(или сразу /etc/rc.d/rc3.d/S99mysql start);

15


администрирование n /usr/local/mysql/bin/mysqladmin — u root — p password n n • • • • • • • n • • • n

“пароль” (при запросе пароля нажать Enter); /usr/local/mysql/bin/mysqladmin — u root — h localhost.localdomain -p password “пароль” (надо ли?); установка интерфейса с Perl: взять на www.mysql.com модули Data-Dumper, DBI и Msql-Mysql-modules; каждый распаковать в отдельную директорию (MsqlMysql-modules последним); зайти в нее; perl Makefile.PL (опционально хочет RPC::PlServer, RPC::PlClient, Storable, Net::Daemon); make; make test (mysqld должен работать); make install (как root). тестирование: зайти в sql-bench; ./run-all-tests — user=test (нужны права для записи в директорию output, час времени и 200 МБ на диске) (connect/disconnect временами грохает mysqld!); можно удалить sql-bench и mysql-test. настроить права доступа (как минимум, убрать анонимный доступ).

Опции ./configure

n — prefix=куда-устанавливать (множество мелочных опций по установке);

n — enable-maintainer-mode [no]; n — enable-shared (делать разделяемые библиотеки)[yes]; Что они означают и каково их действие, вы можете прочитать в документации по postfix или в FAQ.Теперь разбираемся, где и что у нас лежит. # cat > transport.cf user = postfix password = postfix dbname = mail table = transport select_field = transport where_field = domain hosts = localhost ^D # cat > ids.cf user = postfix password = postfix dbname = mail table = aliases select_field = id where_field = alias hosts = localhost ^D # cat > gids.cf user = postfix password = postfix dbname = mail table = aliases select_field = gid where_field = alias hosts = localhost ^D. # cat > aliases.cf user = postfix password = postfix dbname = mail table = aliases select_field = maildir where_field = alias hosts = localhost ^D

16

n — enable-static (10% быстрее)[yes]; n — with-mit-threads (для linux 2.2 не надо); n — with-pthread (для linx 2.2 не надо); n — with-named-thread-libs=где; n — with-named-curses-libs=где; n — with-named-z-libs=где; n — enable-thread-safe-client (если клиентская программа использует потоки); enable-assembler; with-raid; with-unix-socket-path=куда-класть-unix-socket; with-tcp-port=порт [3306]; with-mysqld-user=имя-пользователя-для-mysqld; disable-large-files; with-libwrap; without-debug (15% быстрее); without-server; without-docs; without-bench; without-readline (использовать системный readline вместо встроенного); n — with-charset=кодировка-по-умолчанию (cp1251, koi8_ru, latin1, ...); n — with-extra-charsets=список-дополнительных-кодировок (включая - none, complex, all); n — with-berkeley-db; n — with-innodb; n — with-gemini (Gemini DB). Остальное смотрите на сайте www.mysql.com

n— n— n— n— n— n— n— n— n— n— n— n—

# cat > remote_aliases.cf user = postfix password = postfix dbname = mail table = remote_aliases select_field = rcpt where_field = alias hosts = localhost ^D

Обратите внимание на отсутствие лишних пробелов и других невидимых знаков в концах строчек — это важно! Как, наверное, стало понятно из вышеприведенных файлов, я указал postfix искать MySQL на localhost, подключаться пользователем postfix с паролем postfix, использовать базу данных mail. Конечно, эти данные всего лишь для примера — вы должны или их изменить или понять чем грозит использование паролей, сходными с логинами. Понятно, что эти файлы незачем читать всем (намек на правильный chmod). Проверяем, все ли в порядке. # postfix check.

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

дется разбираться, что не понравилось системе. Теперь необходимо создать пользователя и все необходимые таблицы с помощью вызова mysql — p: mysql> create database mail; Query OK, 1 row affected (0.62 sec) mysql> grant insert, select, delete, update on mail.* to postfix@localhost identified by «postfix»; Query OK, 0 rows affected (0.72 sec) mysql> use mail; Database changed mysql> create table transport (domain varchar(255) PRIMARY KEY, transport char(8));

Здесь будет храниться информация о доменах, обслуживаемых postfix. mysql> create table aliases (id int(6), gid int(6), alias varchar(255) PRIMARY KEY, maildir varchar(255), password varchar(128), infovarchar(128));

Здесь информация о почтовых пользователях системы. mysql> create table remote_aliases (alias varchar(255) PRIMARY KEY, rcptvarchar(255));

А здесь информация о почтовых переадресациях и прочем. Небольшие списки рассылки тоже можно включать сюда.


администрирование Проверьте, имеет ли пользователь postfix доступ к MySQL. # mysql -u postfix -p

Запускаем postfix. # postfix start postfix/postfix-script: starting the Postfix mail system

И в консоли MySQL добавляем домен test.ru. mysql> insert into transport values ( test.ru , virtual: );

И пользователя multik@test.ru. Обратите внимание на путь к почтовому каталогу пользователя и на завершающий «/» в конце строки. mysql> insert into aliases values(1000,12, multik@test.ru , /var/ spool/vmail/test.ru_multik/ , testpassword , info );

Число 1000 я взял из головы — главное, что бы оно было больше последнего UID в системе. В RedHat пользовательские UID начинаются с 500, поэтому я думаю, что 500 локальных пользователей вполне достаточно. А 12 — это GID группы mail на моей системе. Проверяем, что у нас получилось. Заметьте, мы добавили пользователя без затрагивания postfix и других подсистем — это открывает большой простор для написания различных управляющих почтовой системой программ. Создаем каталог, где будет храниться почта и устанавливаем на него права. # mkdir /var/spool/vmail # chown nobody.mail /var/spool/vmail # chmod 770 /var/spool/vmail

И проверяем, как у нас работает прием почты: $ telnet localhost 25 Trying 127.0.0.1... Connected to localhost. Escape character is ^] . 220 mail.test.ru ESMTP Postfix mail from: multik@test.ru 250 Ok rcpt to: multik@test.ru 250 Ok data 354 End data with <CR><LF>.<CR><LF> hello . 250 Ok: queued as 252BFEEAE6

№1, октябрь 2002

В /var/log/messages должны появиться аналогичные строчки: Jun 28 16:24:21 multik postfix/ smtpd[21863]: connect frommultik.iptel.int[127.0.0.1] Jun 28 16:24:23 multik postfix/ smtpd[21863]: 252BFEEAE6: client=multik.ip-tel.int[127.0.0.1] Jun 28 16:24:32 multik postfix/ cleanup[21919]: 252BFEEAE6:messageid=<20020628122423.252BFEEAE6@mail.test.ru> Jun 28 16:24:32 multik postfix/ qmgr[21762]: 252BFEEAE6: from=<multik@test.ru>,size=340, nrcpt=1 (queue active) Jun 28 16:24:32 multik postfix/ virtual[21921]: 252BFEEAE6: to=<multik@test.ru>,relay=virtual, delay=9, status=sent (maildir)

Если мы посмотрим в почтовый спул, то увидим, что письмо принято и дожидается своей очереди. # ls -lR /var/spool/vmail /var/spool/vmail: èòîãî 4 drwx 5 1000 mail Èþí 28 16:26 test.ru_multik

4096

/var/spool/vmail/test.ru_multik: èòîãî 12 drwx 2 1000 mail 4096 Èþí 28 16:26 cur drwx 2 1000 mail 4096 Èþí 28 16:26 new drwx 2 1000 mail 4096 Èþí 28 16:26 tmp /var/spool/vmail/test.ru_multik/cur: èòîãî 0 /var/spool/vmail/test.ru_multik/new: èòîãî 4 -rw 1 1000 mail 389 Èþí 28 16:261025267217.21935_0.multik.iptel.int /var/spool/vmail/test.ru_multik/tmp: èòîãî 0

Теперь подошла очередь установки IMAP и POP3 демонов. Иначе пользователям не через что будет получать почту. (Из одного письма: «А чего у меня sendmail не отдает по pop3 почту Outlook’у ?»). Распаковываем и собираем Courier-IMAP. # tar zxvf courier-imap-1.5.1.tar.gz # adduser courier # chown courier.courier courier-imap1.5.1 # cd courier-imap-1.5.1 # su - courier # cd /{êóäà ðàñïàêîâûâàëè}/courierimap-1.5.1 # ./configure # make

Тут останавливаемся и проверяем, что у нас есть из демонов, которые будут проверять почту:

# authlib/authinfo AUTHENTICATION_MODULES=«authdaemon» AUTHDAEMONMODULELIST=«authcustom authcram authmysql authuserdb authpam» SASL_AUTHENTICATION_MODULES=«CRAMSHA1 CRAM-MD5 PLAIN LOGIN»

Обращаем внимание на наличие authmysql. Иначе разбираемся, почему не так. Выходим из-под пользователя courier и устанавливаем демонов: # exit # make install # make install_configure

А теперь отдадим дань конфигурированию: # cd /usr/lib/courier-imap/etc/ # cp authdaemonrc.dist authdaemonrc

Редактируем authdaemonrc — находим строчку: authmodulelist=«authcustom authcram authuserdb authmysql authpam»

и оставляем от нее только такой вот огрызок: authmodulelist=«authmysql»

Другие строки НЕ ТРОГАЕМ. Теперь указываем, где искать MySQL и информацию о пользователях: # cat > authmysqlrc MYSQL_SERVER MYSQL_USERNAME MYSQL_PASSWORD MYSQL_PORT MYSQL_DATABASE MYSQL_USER_TABLE MYSQL_CLEAR_PWFIELD DEFAULT_DOMAIN MYSQL_UID_FIELD MYSQL_GID_FIELD MYSQL_LOGIN_FIELD MYSQL_HOME_FIELD MYSQL_NAME_FIELD MYSQL_MAILDIR_FIELD ^D

localhost postfix postfix 3306 mail aliases password test.ru id gid alias maildir info maildir

Опция DEFAULT_DOMAIN указывает, что добавлять к логину, если пользователь пытается ввести логин без доменной части. Остальное, я думаю, понятно из названия и описаний. И запускаем pop3. /usr/lib/courier-imap/libexec/ pop3d.rc start

17


администрирование Должен запуститься и не ругаться ни на что. Проверяем: # telnet localhost 110 Trying 127.0.0.1... Connected to localhost. Escape character is ^] . +OK Hello there. user multik@test.ru +OK Password required. pass testpassword +OK logged in. list+ OK POP3 clients that break here, they violate STD53. 1 400 . quit +OK Bye-bye. Connection closed by foreign host.

Как видите, система нас пустила. Наше отправленное письмо лежит и дожидается нас. В maillog должны быть записи похожие на эти:

жет только находить вирусы, но не лечить их, плюс он не смотрит внутрь архивов. Но для функциональности почтового шлюза этого вполне хватит. Но лучше купите лицензию — появится возможность проверять и лечить пользовательские файлы. Тщательно прочитываем /opt/ drweb/doc/postfix/readme.rus. И добавляем Dr.WEB в качестве фильтра к postfix: # # # #

adduser drweb mkdir /var/spool/drweb chown drweb.drweb /var/spool/drweb chmod 770 /var/spool/drweb

Редактируем /etc/postfix/master.cf, как указано в документации к Dr.Web: smtp inet n - n - 50 smtpd ocontent_filter=filter: dummy

и добавляем в конец следующее: Jun 28 17:27:17 multik pop3d: LOGIN, u s e r = m u l t i k @ t e s t . r u , ip=[::ffff:127.0.0.1] Jun 28 17:27:24 multik pop3d: LOGOUT, user=multik@test.ru,ip=[::ffff:127.0.0.1], top=0, retr=0

Радуемся — базовая функциональность достигнута. Можно смело запускать imap сервер аналогично pop, раздавать пользователей и совершать другие необходимые телодвижения. Но лично мне этого мало. Я не хочу видеть вирусы в своем почтовом ящике и в ящиках своих работников и клиентов. Ставим Dr.Web. Забираем с drweb.ru файлы: drweb-4.28.1-linux.tgz drweb-postfix-4.28.4-linux.tgz

И распаковываем их. Хотя могли бы взять rpm файлы — их содержание абсолютно идентично. Проверяем, то ли мы распаковали и туда ли. # cd /opt/drweb # ./drweb Key file: /opt/drweb/drweb.key Registration info: 0100003942 Evaluation Key (ID Anti-Virus Lab. Ltd, St.Petersburg) This is an EVALUATION version with limited functionality! To get your registration key, call regional dealer. Loading /var/drweb/bases/drwebase.vdb - Ok, virus records: 29405

Как видите, Dr.WEB работает в ограниченном режиме. То есть он мо-

18

filter unix - n - n - pipe flags=R user=drweb argv=/opt/drweb/drweb -postfix -f ${sender} ${recipient}

Затем тщательно читаем и редактируем /etc/drweb/drweb_postfix.conf. Лично я изменил следующие параметры: SkipObject = pass ß õî÷ó ïðîïóñêàòü òå îáüåêòû, êîòîðûå drweb íå ïåðåâàðèâàåò. MailbombObject = pass Îäèí èç äîìåíîâ, êîòîðûé îáñëóæèâàåòñÿ ó ìåíÿ, ïðèíàäëåæèò äèçàéíåðàì è ïðî÷åìó õóäîæåñòâåííîìó ëþäó. Îíè î÷åíü îáîæàþò êèäàòüñÿ äðóã â äðóãà àðõèâàìè ñ êàðòèíêàìè, êîòîðûå ñæàòû î÷åíü ñèëüíî. DrWeb îáû÷íî ñ÷èòàåò òàêèå ïèñüìà çà ìýéëáîìáû. AdminMail = root@test.ru Êòî òóò ó íàñ àäìèíèñòðàòîð. FilterMail = DrWeb-DAEMON@ip-tel.ru È îò êîãî áóäåò ïðèõîäèòü ïî÷òà ñ ðóãàíüþ è ïðî÷èìè ñîîáùåíèÿìè. Äàëåå âåçäå: SenderNotify = no Íå íàäî èçâåùàòü ïîñûëàòåëÿ ïèñåì - â 90% ñëó÷àåâ ýòî áåçïîëåçíî è ëèøü çàáèâàåò ïî÷òîâûå êàíàëû. Åñëè îòïðàâèòåëü èçâåñòåí àäðåñàòó, òî îí ñàì íàïèøåò åìó ãíåâíîå ïèñüìî.

Перемещаем /etc/rc.d/drwebd в /etc/ init.d/drwebd и с помощью ntsysv или chkconfig, включаем автостарт Dr.WEB при запуске системы. Тем, кто ставил Dr.WEB через rpm, этого делать не

надо. Проверьте, все ли на месте. Запрещаем всяким лазить куда не следует: cd /var/drweb chown -R drweb.drweb *

И запускаем демона Dr.WEB: /etc/init.d/drwebd start Starting Dr. Web daemon...Key file: / opt/drweb/drwebd.key Registration info: 0100003943 Evaluation Key (ID Anti-Virus Lab. Ltd, St.Petersburg) This is an EVALUATION version with limited functionality! To get your registration key, call regional dealer.Loading /var/drweb/ bases/drwebase.vdb - Ok, virus records: 29405 Daemon is installed, TCP socket created on port 3000

Работоспособность проверяем простым запуском /opt/drweb/drwebpostfix. Он должен запуститься без какого-либо писка и висеть, томительно выжидая и занимая консоль. А в логах должно появиться следующее: Jun 29 13:41:08 multik drweb-postfix: load configuration from/etc/drweb/ drweb_postfix.conf Jun 29 13:41:08 multik drweb-postfix: Actions: infected=Q, suspicious=Q, skip=P,mailbomb=P, scanning_error=T, processing_error=R, empty_from=C, spam_filter=P Jun 29 13:41:08 multik drweb-postfix: dwlib:read_conf(/etc/drweb/ drweb_postfix.conf): successfully loaded Jun 29 13:41:08 multik drweb-postfix: dwlib: startup: set timeout for wholesession to 60000 milliseconds (-1 means infinite) Jun 29 13:41:08 multik drweb-postfix: drweb-pipe: [2250] started ...

У вас так? Значит все работает. Перезапускайте postfix и Dr.WEB встанет на стражу вашей почты. Можете проверить, послав какой-нибудь вирус. Вы получите лишь уведомление о том, что вы посылали вирус. Но просто защиты мне мало. Мне необходима еще и свежая защита. А для свежей защиты необходимо обновлять базы данных о вирусах. Для этого у Dr.Web есть обновлялка, написанная на perl. Для нее нужен модуль String::CRC32. Делающие все правильно могут вспомнить что написано в man CPAN и с помощью install установить этот модуль. Мне оказалось проще и быстрее сделать все вручную: Файл я взял с http://www.cpan.org/ modules/by-module/String/String-


администрирование CRC32-1.2.tar.gz, и установил: # # # # # #

tar zxvf String-CRC32-1.2.tar.gz cd String-CRC32-1.2 perl Makefile.PL make make test make install

ся не буду. Не забудьте — для SquirellMail необходим запущенный imap демон — так что не оплошайте. Для завершения нашей эпопеи осталось всего два шага:

n Проверьте, все ли заработает при

Проверяем: # cd /opt/drweb/update # ./update.pl

update.pl должен сходить в инет на сайт Dr.WEB, забрать все обновления и перезапустить drwebd, если он есть. В логах после запуска вы должны увидеть следующее: Key file: /opt/drweb/drweb.key Registration info: 0100003942 Evaluation Key (ID Anti-Virus Lab. Ltd, St.Petersburg) This is an EVALUATION version with limited functionality! To get your registration key, call regional dealer. Loading /var/drweb/bases/drwtoday.vdb - Ok, virus records: 173 Loading /var/drweb/bases/drw42807.vdb - Ok, virus records: 33Loading /var/ drweb/bases/drw42806.vdb - Ok, virus records: 57 Loading /var/drweb/bases/drw42805.vdb - Ok, virus records: 133 Loading /var/drweb/bases/drw42804.vdb - Ok, virus records: 123 Loading /var/drweb/bases/drw42803.vdb - Ok, virus records: 73 Loading /var/drweb/bases/drw42802.vdb - Ok, virus records: 143 Loading /var/drweb/bases/drw42801.vdb - Ok, virus records: 76 Loading /var/drweb/bases/drwebase.vdb - Ok, virus records: 29405

Видите, появились свежие обновления для drweb с новыми вирусами. Теперь со спокойной душой запихиваем вызов update.pl в crontab. У меня он вызывается каждую ночь. Одно НО: Необходимо периодически вручную отслеживать выход новых версий drweb. Потому что как только выйдет новый DrWeb, ваш автоматически перестанет получать новые вирусные дополнения. И последний шаг — установка www-почты: Я взял последнюю версию SquirellMail с http://www.squirrelmail.org/ и установил согласно прилагающимся инструкциям. Для ее работы необходим настроенный Apache c PHP. Как это делать я уже писал несколько раз. Да и в сети куча документов, посвященным этому вопросу. Установка простая, поэтому я тут останавливать-

№1, октябрь 2002

n

запуске системы. Если есть возможность — перезагрузитесь и проверьте, запустился ли Dr.WEB, postfix, courier pop3 и/или imap c mysql. Отрабатывает ли update’р новые обновления и так далее. Просмотрите все конфигурационные файлы еще раз — не оставили ли вы где-нибудь ляпов или «соплей»? Для самостоятельной работы можете посмотреть на антиспамерные возможности drweb. Проверьте, прикрыт ли MySQL и Dr.WEB от посторонних людей.

В общем все. Можете откинуться на спинку кресла и наблюдать, как работает почта. Но для меня это еще не все. Раз все равно для пользователей стоит Apache с PHP, то я написал простенькую WWW — утилитку для управления почтовыми пользователями. Для ее работы необходимо создать в MySQL такую таблицу: mysql> create table admins (login varchar(20) NOT NULL, password varchar(20),rights int(6));

И руками занести туда значения вроде “admin”, ’password’,0. Эта таблица у меня используется в других внутрикорпоративных сервисах, поэтому желающие могут залезть в код и все исправить. По адресу www.samag.ru/2002/01/ multik/ лежат 3 файла auth.php, global.php, index.php. Просто положите эти три файла в один каталог, доступный вам. Поправьте значения в global.php. Все, можете вызывать и пользоваться. Логин и пароль — те, которые вы вручную добавили в таблицу admins. Ну а дальше, я думаю, вы поймете. Утилитка писалась «на коленке», поэтому я вполне понимаю, что можно написать лучше и красивее. Пишите. Удачи ! А тем, кто до сюда дочитал — маленький бонус. Если вы поглядите на это:

$ telnet localhost 25 Trying 127.0.0.1... Connected to localhost. Escape character is ^] . 220 mail.test.ru ESMTP Postfix ehlo multik 250-mail.test.ru 250-PIPELINING 250-SIZE 10240000 250-ETRN 250-AUTH LOGIN PLAIN 250-AUTH=LOGIN PLAIN 250-XVERP250 8BITMIMEÊ

К вашему большому сожалению вы не увидите строчек: 250-AUTH LOGIN PLAIN DIGEST-MD5 CRAMMD5 250-AUTH=LOGIN PLAIN DIGEST-MD5 CRAMMD5

Либо вы увидите эти строчки, но почему-то нормальные почтовые клиенты (к примеру, TheBat или stuphead) не смогут авторизоваться для отправки почты. Только для отправки! В чем же дело ? Дело в одной маленькой библиотеке, называемой Cyrus-SASL. К сожалению, ее писали люди, которые писали ее неправильно. Метод sasldb в ней важнее всех. Внимание, если /etc/sasldb не пустой (смотреть вывод sasldblistusers), то в выводе postfix появляются строчки про DIGEST-MD5 и CRAM-MD5. Если пустой -то не появляются. Если в sasldb есть хоть одна запись, то аутентификация с участием методов LOGIN, DIGEST-MD5 и CRAM-MD5 идет через sasldb, НЕВЗИРАЯ на то, что написано после pwcheck_method. Поэтому, если у вас есть активно мигрирующие пользователи с нормальными почтовыми клиентами (Outlook и Mozilla — не подходят — они оба используют только метод PLAIN), то для отправки почты с таких клиентов их надо заводить вручную, используя команду аналогичную этой: saslpasswd -c -u `postconf -h myhostname` username

Вот такой вот SASL. Правда, обещают, что в v2 будет все по честному, но пока v2 не выбралась из бета-состояния, да и postfix в стабильных версиях не поддерживает Cyrus-SASL v2. Courier-IMAP использует SASL по другому, поэтому с ним все в порядке. Вот теперь точно все. Удачи!

19


администрирование

МИГРАЦИЯ С WINDOWS НА LINUX

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

ДМИТРИЙ ГАЛЫШЕВ Данная статья предназначена в первую очередь для тех, кто, считая, что «качественный продукт не может быть бесплатным», доверил управление своими серверами дорогой операционной системе и вынужден периодически смотреть на «синий экран смерти», терять время на перезагрузки и иногда на восстановление самой ОС. Приведем сравнение операционных систем Microsoft Windows NT и

20

Linux по нескольким критериям, не претендуя на детальное сравнение, которому посвящено уже много статей.

Windows NT: n Является коммерческим ПО, за исn

пользование каждой его копии необходимо платить. Производитель обещает бесплат-

n

ную техническую поддержку зарегистрированных пользователей, но не несет ответственности за потери данных, произошедшие из-за ошибок в коде ОС, и полученный ущерб в случае чего придется доказывать в суде. В комплект поставки входит операционная система, утилиты администрирования, набор простых игр, www- и ftp-сервер IIS.


администрирование n Нужные Вам утилиты, не входяn n

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

Linux: n Является свободно распространя-

n

n

n

n

n

емым ПО, и, имея одну копию, Вы можете использовать ее на любом количестве компьютеров и тиражировать без ограничений. Ядро ОС и поставляемые с ним программы Вы используете на свой страх и риск без каких-либо гарантий. Это не значит, что техническая поддержка отсутствует — вы можете обратиться за ней к автору программы. В дистрибутив входит комплект интернет-утилит как серверной, так и клиентской направленности, инструменты для разработки сетевого взаимодействия с другими ОС, игры и многое другое. Если какого-то нужного Вам пакета нет в дистрибутиве, Вы можете бесплатно получить его с сайта разработчика. В состав каждого дистрибутива входит комплект документации по ОС и всем установленным программам. Пользователи могут не только выполнять задачи на сервере, но и планировать их выполнение в свое отсутствие.

Если Вы хотите узнать больше, обратитесь к статье «Microsoft Windows NT Server 4.0 против UNIX» Джона Кирха, сетевого консультанта и сертифицированного специалиста Microsoft (Windows NT) на http:// www.linux.org.ru/books/unix-nt.html. Статья датирована 1998 годом, и с тех пор многое изменилось, но расстановка сил осталась прежней. Управление Linux основано не на «интуитивно понятном интерфейсе», а на правке различных конфигурационных файлов после чтения документации, которая написана в основном на английском языке, хотя переводов с каждым годом становится больше и больше. Если Вы нелюбознательны

№1, октябрь 2002

или нетерпеливы, эта ОС не для Вас. Не испугались? По прежнему полны решимости? Ну что же, начнем осваивать новую ось. В любом случае договоримся, что все работающие сервера остаются работать до тех пор, пока к ним обращается хотя бы один пользователь. Для начала рассмотрим задачу освобождения сервера под управлением Windows NT от обязанностей основного контроллера домена (PDC). Основное преимущество доменной структуры — выполнение скриптов, определенных администратором для выполнения при входе и выходе пользователя домена. Также, с помощью Samba, мы возьмем на себя задачи файл-сервера — предоставление коллективного доступа к своим файлам и папкам. Применить здесь понятие «к ресурсам» было бы не совсем корректно, так как к ресурсам относятся время процессора, память и так далее. Удаленные пользователи могут использовать и их, но Windows NT такой возможности не предусматривает. Системы Microsoft Windows 3.11, 9х и NT используют для предоставления совместного доступа к файлам, папкам и принтерам протокол Server Message Block (SMB). В Linux за работу с SMB отвечает пакет Samba. О нем его создатели пишут следующее: «Вот короткий список того, что делает Samba. Для многих сетей можно сказать коротко: «Samba предоставляет полноценную замену серверам Windows NT, Warp, NFS или Netware».

n SMB сервер для предоставления

n

n

n

доступа к файлам и принтерам, клиентам с рабочих станций Windows 95, Warp Server и подобным. Сервер имен NetBIOS, который ко всему прочему поддерживает список доступных компьютеров в сети. Samba может выступать в роли главного обозревателя Вашей сети. SMB клиент позволяет пользоваться ресурсами других компьютеров сети (как файлами, так и принтерами) из Unix, Netware и других ОС. Расширение клиента «tar» позво-

n

ляет делать резервные копии удаленных ресурсов. Утилиты командной строки, поддерживающие некоторые возможности администрирования NT, которые могут быть использованы в Samba, NT workstation и NT server.

Вы можете получить больше информации на нашем сайте: http://samba.org/ samba». Если Samba еще не установлена, обращаемся к компакт-диску с дистрибутивом и устанавливаем все пакеты, содержащие в имени слово «samba». Типичные настройки обычно описаны в файле /etc/samba/ smb.conf-sample и подходят большинству пользователей. Рассмотрим его поподробнее. Файл состоит из секций, которые определяются именем в квадратных скобках. В секции [global] объявляются общие переменные, влияющие на работу демонов. В секции [homes] предоставляется возможность монтирования удаленными пользователями своих личных папок, находящихся на сервере. Секция [printers] открывает доступ ко всем установленным в системе принтерам. Переименуем файл /etc/samba/ smb.conf-sample в smb.conf и начнем его править под себя. Полезными могут оказаться «подстановочные переменные», которые при чтении заменяются актуальными для сеанса значениями. Например, «%u» будет заменено именем подключающегося пользователя, а «%L» — NetBIOS-именем сервера. Полный список этих переменных Вы найдете в man-руководстве к smb.conf. Вот некоторые глобальные параметры, на которые стоит обратить внимание: workgroup — задает имя рабочей групппы или домена, в который входит компьютер. hosts allow — список компьютеров и сетей, которым разрешен доступ, разделенный пробелами. guest account — имя пользователя с правами гостя. По умолчанию — это nobody. Пользователь должен быть зарегистрирован в системе. Можно ука-

21


администрирование зать с помощью параметра username map файл, содержащий толкование передаваемых клиентом имен для сервера. Файл будет читаться построчно; в строке слева от знака «=» должно стоять имя, воспринимаемое сервером, справа — соответствующее ему имя, которое может передавать клиент. Например, если в файле содержится строка nobody = guest pcguest пользователям guest и pcguest будет предоставлен доступ как пользователю nobody. security — один из важных параметров конфигурации — модель аутентификации. Возможны варианты: security = user — для получения списка ресурсов пользователь должен быть опознан сервером. Будьте внимательны: по умолчанию ресурсы, открытые гостям, не будут доступны тем, кого сервер «не знает». security = share — получение списка ресурсов не требует аутентификации. Клиент посылает запрос при подключении к конкретному ресурсу. security = server — с точки зрения клиента, эта модель ничем не отличается от первой. Разница в том, что полученные имя/пароль Samba попытается проверить на доверенном сервере (например НТ). Если попытка провалится, для этого соединения модель сменится на «user». security = domain — отличается от предыдущей тем, что аутентификация происходит на PDC или BDC. Пароли должны передаваться в зашифрованном виде. encrypt passwords — шифрование паролей. По умолчанию этот параметр выставлен в «no» и пароли передаются по сети открытым текстом. interfaces — привязка к конкретным интерфейсам. Можно указать имя интерфейса со звездочкой вместо номера, тогда привязка будет ко всем найденным интерфейсам этого типа. Также можно указать просто IPадрес или пару IP/маска сети. local master — определяет, претендовать ли на роль локального обозревателя сети. os level — так называемый «уровень» ОС. Для справки: у НТ-сервера этот параметр равен 32, у PDC — 64. Максимальное значение — 255.

22

domain master — определяет, претендовать ли на роль основного обозревателя домена. Если сеть состоит из нескольких сегментов, то локальные обозреватели каждого сегмента будут синхронизировать свои списки с ним. preferred master — сообщает демону nmbd, что он является приоритетным обозревателем рабочей группы или домена, и это вынуждает его устраивать выборы при запуске, а в случае проигрыша — каждые 6 минут. wins support — включает поддержку Samba-сервером WINS — Windows Internet Naming Service, службы определения адресов, сопоставляющей имена компьютеров в сети Microsoft с адресами IP. wins server — указывает IP-адрес WINS-сервера сети. Этот параметр не может указывать на свой IP, т.е. используется только в случае, когда wins support = no. domain logons — позволяет Samba выступать в роли контроллера домена. logon script — задает имя скрипта, выполняемого при входе пользователя в домен. logon path — указывает на каталог, где будут храниться профили пользователей. Из параметров, применимых к конкретным разделяемым ресурсам, следует отметить: comment — краткое описание ресурса, выводимое большинством клиентов рядом с его именем. path — реальное расположение разделяемого ресурса в файловой системе сервера. browseable — определяет, будет ли выдаваться имя ресурса на запрос о доступных ресурсах сервера. writeable — определяет, возможна ли запись или модификация файлов ресурса пользователем. guest ok — разрешает доступ к ресурсу всем пользователям сервера. (См. глобальный параметр security). valid users — список пользователей, которым разрешен доступ к ресурсу. Как и в любой список пользователей в файле smb.conf можно добавлять и группы, предварив имя группы символом «@». write list — список пользователей, которым разрешена запись и модифи-

кация файлов ресурса. Значение параметра writeable для этих пользователей не учитывается. create mask — стандартная UNIXмаска, определяющая права доступа к вновь создаваемому файлу. Directory mask определяет права доступа к вновь создаваемому каталогу. Полный список опций, употребляемых в файле, находится в man-руководстве к smb.conf. После редактирования рекомендуется проверить его на предмет ошибок и противоречий с помощью программы testparm. Если ошибки есть, она сообщит о них и, скорее всего, подскажет причину их возникновения. Сначала настраиваем Samba как обычного сетевого клиента — она не должна претендовать ни на роль локального обозревателя, ни тем более — контроллера домена. Если таковые уже есть, то присваиваем значение «no» параметрам local master, domain master и preferred master. Если разграничения доступа еще нет, самое время подумать о нем. В большинстве случаев несложно определить группы пользователей, работающие с определенными группами файлов. Например, бухгалтерия работает с базами бухгалтерской программы и не интересуется проектами программистов. Мне не известен сколько-нибудь реальный способ переноса базы пользователей из Windows NT в Linux, поэтому придется делать это вручную. Неудобно, но в этом есть свои положительные стороны — найдется несколько неиспользуемых учетных записей, а также можно будет перераспределить пользователей по группам. На момент написания статьи Samba (версии 2.2.5) имеет собственную базу пользователей, но не поддерживает пользователей, не имеющих учетной записи в системе. Разработчики обещают со временем перейти на независимую базу пользователей, но пока придется пользователей заводить сначала в системе. Пользователям, у которых не будет доступа к терминалу сервера (в идеале всем, кроме администратора) назначаем /bin/false в качестве командного интерпретатора и блокируем учетную запись, после чего пропи-


администрирование сываем их в базе Samba командой smbpasswd дважды: с ключом «-a» — для занесения в базу, с ключом «-e» — для снятия блокировки с записи. Хорошо, если новая операционная система ставится на специально купленный сервер, еще лучше, если сервер куплен с предустановленной нужной нам ОС. Но как быть, если сервер один и нельзя его трогать до тех пор, пока все пользователи не смогут получить гарантированный доступ к своим файлам? Придется использовать любой доступный компьютер как «временный сервер». На него ставим тот же дистрибутив, что предполагаем ставить на основной сервер, и постепенно перенесем все ресурсы, предоставляемые старым сервером. Какая машина справится с этой работой зависит от предполагаемой нагрузки. Достоверно известно, что Celeron 700/196Mb RAM/30Gb HDD в состоянии обслуживать 15 клиентов, интенсивно работающих с большим количеством файлов при средней нагрузке ~10%. Разделяемые SMB ресурсы делим на:

Если в Вашей сети используются домены, проверьте smb.conf сервера:

n Общедоступные (папка с драйве-

Если все в порядке, можно добавлять рабочие станции в домен. Рабочие станции, функционирующие под управлением Windows NT/ 2000, используют так называемые «машинные» учетные записи — метод проверки подлинности рабочей станции (не пользователя), чтобы избежать входа в домен рабочей станции с таким же именем NetBIOS и получения прав пользователя домена. Рабочие станции, функционирующие под управлением Windows 9x/Me, таких учетных записей не используют, поэтому не могут считаться полноправными членами домена. Существует два метода создания «машинных» учетных записей — ручной и автоматический. Первый метод подразумевает выполнение вручную трех команд на сервере для каждой рабочей станции домена:

n

n

рами и дистрибутивами программ, «помойка»). Доступные группе (папки с данными, с которыми работает ограниченное количество лиц, например папка с базами бухгалтерской программы). Домашние (папки привилегированных пользователей, доступные только им).

Наиболее часто допускаемая ошибка на этапе переноса данных — невнимательность к атрибутам вновь перенесенных файлов. Владельцем для них лучше всего назначить пользователя root, а группу-владельца — ту, что будет с этими файлами работать. Ну и не забыть позволить перезаписывать файлы членам группы-владельца. Необходимо пройтись по пользователям и проверить доступ с их рабочих станций к новому серверу: чтение/ запись в папку; чтение/запись в файлы, созданные другими членами группы, а также членами групп, имеющих смежный доступ в папку; отсутствие доступа в неразрешенные папки.

№1, октябрь 2002

n Шифрование паролей должно быть включено. [global] ... domain master = yes ...

n Сервер должен поддерживать вход пользователей в домен и предоставлять разделяемый ресурс NETLOGON. [global] ... domain logons = yes ... [netlogon] path = /usr/local/samba/lib/ netlogon read only = yes write list = ntadmin

n Сервер должен быть главным обозревателем домена. [global] ... encrypt passwords = yes ...

root# useradd -g 100 -d /dev/null -c <èìÿ_ìàøèíû> -s /bin/false <èìÿ_ìàøèíû>$ root# passwd -l <èìÿ_ìàøèíû>$ root# smbpasswd -a -m <èìÿ_ìàøèíû>

Вместо <имя_машины> подстав-

ляется NetBIOS-имя рабочей станции домена. Второй метод рекомендован разработчиками Samba как более безопасный и требует лишь добавления в секцию [global] строки: add user script adduserscript %u

=

/usr/bin/

Это подразумевает, что в каталоге /usr/bin сущес твует скрипт adduserscript примерно следующего содержания: #!/bin/sh /usr/sbin/useradd -d /dev/null -g 100 -s /bin/false -m $1

После этого с добавляемой рабочей станции заходим в домен как привелигированный пользователь (root, если не указан другой параметром admin user), и создаем учетную запись рабочей станции домена. Пока это не сделано, попытки зайти как обычный пользователь провалятся, даже если пользователь существует. После того, как вся нагрузка будет перенесена на «временный» сервер, а старый сервер — выключен, подождем денёк-другой возникновения проблем и только послеэтого начинаем установку Linux на основной сервер. После установки системы конфигурационные файлы и файлы пользователей просто переносятся с «временного» сервера. Итак, все готово, начинаем. Прикидываем время на переброску всех необходимых файлов со старого сервера на новый и объявляем профилактику на нужное время плюс 10 минут на неожиданности (если мы не допустили оплошностей при подготовке, работа возобновится как только файлы будут скопированы). Старому серверу меняем NetBIOSимя и перезапускаем Samba. Новому присваиваем имя старого. Перезапускаем Samba. Все. Можем переходить к замене любимого лакомства для Nimda и CodeRed — Microsoft Information Server на самый популярный в Internet www-сервер Apache, проксисервер Squid и ftp-сервер proftpd. Но это уже немного другая история...

23


администрирование

ЧТО ТАКОЕ SAMBA?

СЕРГЕЙ ЯРЕМЧУК GRINDER@UA.FM


администрирование Я думаю, что утверждение о том, что самой популярной операционной системой для клиентских машин является Windows, не вызовет больших споров. Хотя с появлением таких продуктов, как OpenOffice.ru, и активному продвижению своих продуктов компаниями AltLinux и ASPLinux, положение несколько изменилось, но до массовости еще дело не дошло. Поэтому большинство пользователей продолжают набирать документы в Word’e и бродить по Интернету с помощью Internet Explorer. А вот в качестве сервера положение последней уже не так однозначно, здесь уже играют роль NetWare от Novell и, конечно же, Unix. Плюс за последний год пришлось всем потесниться из-за появления новых «игроков» на этом рынке - операционных систем с открытым кодом - Linux, FreeBSD и OpenBSD, которые уже сейчас занимают немалый процент рынка серверов. Прежде всего по причине своей надежности, устойчивости, совместимости со множеством платформ и безопасности. Поэтому сейчас системному администратору приходится часто решать вопросы интеграции серверов под управлением Linux/Unix в сеть, где преобладают клиентские машины от Microsoft, особенно в качестве файл-серверов или серверов печати. Так как чуда от Microsoft особенно ждать не приходится и Windows врядли научится работать с сетевой файловой системой Unix (NFS) стандартными средствами, то по принципу горы и Магомета, просто научили Unix притворяться, будто бы он - Windows NT. Взаимодействие компьютеров в сети Windows построено на использовании протокола SMB (Server Message Block - блоки серверных сообщений), который обеспечивает выполнение всех необходимых в этих случаях задач по открытию и закрытию, чтению и записи, поиску файлов, созданию и удалению каталогов, постановке задания на печать и удалению его оттуда. Все необходимые для этого действия реализуются в Unix-подобных операционных системах посредством использования пакета SAMBA. Что же должен обеспечить SAMBA сервер для нормальной работы в сети

№1, октябрь 2002

Windows машин? Во-первых, контроль доступа, который может быть реализован либо на уровне ресурсов (share level), когда какому-либо ресурсу в сети назначается пароль и соответствующие правила использования («только для чтения», например), приэтом имя пользователя не имеет абсолютно никакого значения. Либо более совершенная и гибкая организация на уровне пользователя, когда для каждого пользователя создается учетная запись, где помимо имени и пароля содержится вся необходимая информация о правах доступа к ресурсу. И прежде чем получить доступ к требуемому ресурсу каждый пользователь проходит аутентификацию, после успешного прохождения которой ему и предоставляется права на использование согласно учетным записям. Во-вторых, необходима эмуляция прав доступа, определяемых файловой системой. Все дело в том, что у рассматриваемых систем права доступа к файлам и каталогам на диске организованы по-разному. В Unix традиционно существует три категории пользователей файла - владелец (owner), группа (group) и остальные (other). Каждому из этих субъектов могут быть предоставлены права на чтение (read), запись(write) и выполнение ( execute). В Windows NT система доступа несколько гибче, доступ предоставляется нескольким группам или пользователям, причем соответствующие права доступа определяются раздельно для каждого субъекта. Поэтому полноценно эмулировать средствами SAMBA права доступа заложенные в NTFS, невозможно. А с клиентами, работающими под управлением Windows 9x, дело обстоит иначе. Еще со времен дедушки ДОС, по причине того, что система однопользовательская и ни о каких пользователях, а тем более группах, не могло быть и речи, для файловой системы FAT определено всего четыре атрибута - только чтение (read only), системный (system), архивный (archive) и скрытый (hidden). Плюс ко всему в Windows, в отличие от Unix, имеет особое значение расширение файла, так файлы, предназначенные для выполнения, имеют расширение - exe, com, bat. Соответствия между правами доступа Unix и DOS выражаются так:

n только для чтения - чтение, запись для владельца;

n архивный - выполнение для владельца;

n системный - выполнение для группы;

n скрытый - выполнение для группы. Вот с проблемами так или иначе разобрались. Давайте разберемся теперь конкретно с реализацией и настройкой SAMBA в Linux. Для работы Samba необходимо, чтобы были запущены два демона: smbd обеспечивает работу службы печати и разделения файлов для клиентов Samba, таких как Windows всех мастей; демон nmbd обеспечивает работу службы имен NetBIOS, а также может использоваться для запроса других демонов служб имен. Для доступа к клиентам используется протокол TCP/IP. Как правило, Samba устанавливается вместе с дистрибутивом Linux. Как проверить? Просто дайте команду: sergej@grinder sergej]$ whereis samba

и вы должны получить что-то вроде этого: samba: /usr/sbin/samba /etc/samba / usr/share/man/man7/samba.7.gz

Если нет, то идите на ftp:// f tp.samba.org/pub/samba/sambalatest.tar.gz или практически на любой сервер с программами для Linux и качайте в виде rpm или исходников. Пакет прост в установке, поэтому, чтобы не занимать места, будем считать, что он у вас установлен. Теперь давайте проверим, запущен ли демон: [sergej@grinder sergej]$ ps -aux | grep smbd root 1122 0.0 0.6 4440 380 ? S 16:36 0:00 smbd -D

У меня уже запущен. Если у вас нет, то в Linux Mandrake, например, чтобы он запускался при старте отметьте нужный пункт в: DrakConf стартовые сервисы или control-panel - Servise Configuration в Red Hat, обычно этого бывает достаточно, или запускайте вручную: ./etc/rc.d/init.d/smb start. Единственный конфигурационный файл Samba называется smb.conf и находится в каталоге /етс иногда (в AltLinux, например, в каталоге /etc/

25


администрирование samba). Сервис SAMBA считывает его каждые 60 секунд. Поэтому изменения внесенные в конфигурацию вступают в силу без перезагрузки, но не распространяются на уже установленные соединения. Вот за что я люблю Linux - так это за то, что конфигурационные файлы являются обычными текстовыми( к тому же хорошо комментированные внутри), и для того чтобы задействовать большинство параметров достаточно только раскомментировать соответствующую строчку. Файл smb.conf - не исключение. Он состоит из именованных разделов, начинающихся из имени раздела, заключенного в квадратные скобки. Внутри каждого раздела находится ряд параметров в виде key=value. Файл конфигурации содержит четыре специальных раздела: [global], [homes], [printers] и отдельные ресурсы (shares). Как следует из названия, раздел [global] содержит наиболее общие характеристики, которые будут применяться везде, но которые, впрочем, затем можно переопределить в секциях для отдельных ресурсов. Значения типичных параметров секции global: workgroup = èìÿ_ãðóïïû # íàçâàíèå ðàáî÷åé ãðóïïû â ñåòè Windows netbios name = èìÿ ñåðâåðà â ñåòè server string = êîììåíòàðèé, êîòîðûé âèäåí â îêíå ñâîéñòâ ïðîñìîòðà ñåòè guest ok = yes # ðàçðåøåíèå ãîñòåâîãî âõîäà ( guest ok = no - ãîñòåâîé âõîä çàïðåùåí) guest account = nobody # èìÿ ïîä êîòîðûì ðàçðåøåí ãîñòåâîé âõîä â ñèñòåìó security = user # Óðîâåíü äîñòóïà. user - íà óðîâíå ïîëüçîâàòåëÿ, security = share - àóòåíòèôèêàöèÿ íà îñíîâå èìåíè è ïàðîëÿ.

При хранении базы паролей на другом SMB-сервере используются значения security = server и password server = name_server_NT. В случае если сервер является членом домена используется значение security = domain, пароль для доступа указывается в файле определенном с помощью опции smb passwd file = /path/to/ file. Кроме того, при регистрации могут использоваться шифрованные (encrypted) и незашифрованные (plaintext) пароли. Последние используются в старых Windows (Windows for Workgroups, Windows 95 (OSR2), все версии Windows NT 3.x, Windows NT 4

26

(до Service Pack 3). Для включения варианта использования шифрованного пароля используется опция encrypt password =yes (по умолчанию используются не шифрованные пароли). Для правильного отображения русских имен файлов используются следующие опции: client code page = 866 и character set = koi8-r. Опция interfaces = 192.168.0.1/24 указывает в какой сети (интерфейсе) должна работать, программа если сервер подключен сразу к нескольким сетям, а при установке параметра bind interfaces only = yes, сервер будет отвечать на запросы только из этих сетей. hosts allow = 192.168.1. 192.168.2. 127. - определяет клиентов, для которых разрешен доступ к сервису. В секции global возможно использование различных переменных для более гибкой настройки работы сервера, после установки соединения вместо них подставляются реальные значения. Например, в директиве log file = /var/log/samba/%m.log , параметр %m помогает определить отдельный лог-файл для каждой клиентской машины. Наиболее употребительные переменные, используемые в секции global: %a - àðõèòåêòóðà ÎÑ íà êëèåíòñêîé ìàøèíå (âîçìîæíûå çíà÷åíèÿ Win95, Win NT, UNKNOWN è ò.ä.) %m - NetBIOS-èìÿ êîìïüþòåðà êëèåíòà. %L - NetBIOS-èìÿ ñåðâåðà SAMBA. %v - âåðñèÿ SAMBA. %I - IP-àäðåñ êîìïüþòåðà êëèåíòà. %T - äàòà è âðåìÿ. %u - èìÿ ïîëüçîâàòåëÿ, ðàáîòàþùåãî ñ ñåðâèñàìè. %H - äîìàøíÿÿ äèðåêòîðèÿ ïîëüçîâàòåëÿ %u.

Включение параметров preserve case и short preserve case заставляют сервер сохранять всю вводимую информацию с учетом регистра символов (в Windows регистр не имеет значения, а во всех Unix это не так). Раздел [homes] позволяет пользователям подключаться к своим рабочим каталогам без явного их описания. При запросе клиентом своего каталога, например //sambaserver/ sergej, система ищет соответствующее описание в файле и, если не находит его, то просматривает наличие этого раздела. Если раздел существует, то просматривается файл паролей для поиска рабочего каталога пользо-

вателя, сделавшего запрос и при нахождении делает его доступным для пользователя. Типичное описание раздела: [homes] comment = Home Directories # êîììåíòàðèé êîòîðûé âèäåí â îêíå ñâîéñòâ ñåòè browseable = no # îïðåäåëÿåò âûâîäèòü ëè ðåñóðñ â ñïèñêå ïðîñìîòðà. writable = yes # ðàçðåøàåò (no çàïðåùàåò) çàïèñü â äîìàøíþþ äèðåêòîðèþ create mode = 0750 # ïðàâà äîñòóïà äëÿ âíîâü ñîçäàííûõ ôàéëîâ directory mode = 0775 # òîæå, íî òîëüêî äëÿ êàòàëîãîâ

После настройки параметров по умолчанию вы можете создать сетевые ресурсы, доступ к которым может получить определенные пользователь или группа пользователей. Создается такой ресурс из уже существующего каталога, для этого в файле пишем: [public] comment = Public Stuff path = /home/samba public = yes writable = no printable = no write list = administrator, @sales

Параметр path указывает на каталог, в котором располагается ресурс, параметр public указывает, может ли пользоваться ресурсом гость, а printable - может ли использоваться данный ресурс для печати. Параметр write list позволяет задать пользователей, которым разрешена запись в ресурс независимо от значения writable (в данном примере это пользователь administrator и группа sales). Возможно использование и противоположного списка - read list. Если есть необходимость скрыть некоторые файлы, то в Unix/Linux для этого имя файла должно начинаться с точки (параметр hide dot files, который регулирует отображение скрытых файлов, по умолчанию = yes). Но кроме этого есть возможность задать шаблоны имен скрытых файлов, для этого используется параметр hide files. Каждый шаблон начинается и заканчивается с символа косой черты «/» и может содержать символы, применяемые в регулярных выражениях. Например: hide files = /*.log/??.tmp/. Но все эти ухищрения обходятся пользователям очень просто - установкой режима «показывать скрытые и системные файлы» проводника Windows. Для уверенного ограничения доступ-


администрирование ности (возможности удаления) файла (каталога) используйте параметры: veto files и delete veto files. С CD-ROM дисками дело обстоит несколько сложнее. Все дело в том, что в Unix-подобных системах понятие диска отсутствует как таковое, и для того чтобы получить доступ к нужному устройству, оно первоначально должно быть смонтировано в дерево каталогов (# mount -t iso9660 /dev/cdrom /mnt/ cdrom). А после использования, чтобы не разрушить файловую систему, должно быть размонтировано (# umount /dev/cdrom), иначе устройство просто не отдаст диск. Если у вас на сервере запущен демон autofs, то проблема решается просто. Для того чтобы устройство которое не используется в течении некоторого времени, было автоматически размонтировано, установите нужное значение параметра timeout в файле /etc/auto.master, например: /mnt

/etc/auto.misc timeout=60

А затем установите параметры для соответствующего устройства в файле /etc/auto.misc: cdrom fstype=iso9660,ro,nosuid,nodev dev/cdrom

: /

После всего прописываем в /etc/ smb.conf следующие строки, чтобы сделать доступным данный ресурс: [cdrom] path = /mnt/cdrom writable = no

Второй вариант состоит в использовании директив preexec и postexec, которые указывают какие команды необходимо выполнить при обращении к ресурсу и после отсоединения от него. [cdrom] path = /mnt/cdrom read only = yes root preexec = mount /mnt/cdrom #ìîíòèðîâàòü ðåñóðñà èìååò ïðàâî òîëüêî root root postexec = umount /mnt/cdrom # åñòåñòâåííî ýòè òî÷êè ìîíòèðîâàíèÿ äîëæíû áûòü îïèñàíû â ôàéëå /etc/fstab èíà÷å íåîáõîäèìî óêàçàòü è îñòàëüíûå äàííûå.

Теперь при обращении к ресурсу автоматически монтируется CD-ROM. А в идеальных случаях и размонтируется. Вся проблема в том, что реше-

№1, октябрь 2002

ние о закрытии ресурса должен принять сервер и все потому, что клиенты, как правило, не извещают об этом. Но наиболее частой причиной является то, что ресурсом могут одновременно пользоваться сразу несколько пользователей или на одном компьютере оставлен открытый файл на данном ресурсе. Поэтому CD-ROM автоматически не размонтируется, единственный приемлемый способ, чтобы освободить ресурс - посмотреть с помощью утилиты smbstatus номер процесса, использующего данный ресурс, и убить его командой # kill pid_number (или kill -s HUP pid_number). Установив необходимую конфигурацию необходимо теперь создать учетные записи пользователей (за исключением гостевого входа с минимальными правами nobody). Для интентификации пользователей SAMBA используется файл /etc/samba/ smbpasswd, в котором содержатся имена и зашифрованные пароли пользователей. Так как механизм шифрования в сетях Windows-машин не совместим со стандартными Unix механизмами, то для заполнения файла паролей используется отдельная утилита - smbpasswd. # useradd -s /bin/false -d /home/samba/ sergej -g sales sergej # smbpasswd -a sergej

В этом примере добавляется новый пользователь sergej с фиктивной оболочкой, принадлежащий группе sales и домашним каталогом /home/ samba/sergej. Затем создается пароль для пользователя sergej. С помощью SAMBA можно организовать возможность сетевой печати с компьютеров под управлением Windows (если планируется отдельный сервер печати, то для этого бывает достаточно и машины на базе 486 процессора). Для этого в секции [global] необходимо записать такие строки: printcap name = /etc/printcap # ôàéë îïèñàíèÿ ïðèíòåðîâ, ïîäêëþ÷åííûõ ê ñèñòåìå load printers = yes #, óêàçûâàåò íà íåîáõîäèìîñòü àâòîìàòè÷åñêîãî âêëþ÷åíèÿ â ñïèñîê ñåòåâûõ ðåñóðñîâ printing = lprng # ñèñòåìà ïå÷àòè (äëÿ Linux ìîæåò åùå èñïîëüçîâàòüñÿ bsd)

Далее каждый принтер описывается как дисковый ресурс с единствен-

ным исключением, параметр printable = yes. Например: [printers] path = /var/spool/samba # óêàçûâàåò íà êàòàëîã â êîòîðûé ïîìåùàþòñÿ çàäà íèÿ íà ïå÷àòü browseable = yes printable = yes read only = yes

После создания файла протестируйте его с помощью утилиты testparm, но при помощи данной программы можно обнаружить лишь синтаксические ошибки, а не логические, поэтому нет никакой гарантии, что описанные в файле сервисы будут корректно работать (при тестировании будут выведены все установки даже те, которые установлены поумолчанию). Но если программа не ругается, можете надеяться, что при запуске файл будет загружен без проблем. А правильность установки принтера можно проверить с помощью утилиты - testprns. Плюс не забывайте о log-файлах - при возникновении проблем там иногда можно найти решение. Теперь немного о хорошем. Конфигурирование Samba - довольно сложная процедура, но с дистрибутивом поставляется инструмент администрирования на основе Web, который называется swat (Samba Web Administration Tool). Swat запускается в виде сервиса или с помощью сервера Apache, и предназначен для редактирования файла smb.conf, а также для проверки состояния, запуска и остановки демонов Samba. Для работы в виде сервиса в файле /etc/ services должна быть обязательно строка swat 901/tcp, а в файле /etc/ inetd.conf - swat stream tcp nowait.400 root /usr/local/samba/bin/swat swat. Теперь для запуска Swat в окне браузера введите: http://localhost:901

После всех изменений в файле smb.conf иногда потребуется перезапустить демон smb: /etc/rc.d/init.d/ smb restart. Если после всех перечисленных действий так и не удалось организовать доступ к ресурсам SAMBA, то в дальнейшей настройке помогут такие утилиты, как ping, nmblookup или на крайний случай tcpdump. Вот и все.

27


администрирование

В АДМИНИСТРИРОВАНИИ СЕРВЕРА: ПОЧЕМУ БЫ И НЕТ? РОМАН СУЗИ Стандартных решений не существует. Каждый системный администратор рано или поздно начинает писать свои скрипты, которые облегчают его работу, избавляя от рутины. Для автоматизации в Unix-системах традиционно применяются командные оболочки (типа bash или ksh, разновидностей оболочек достаточно много) и язык Perl. Причем, следуя философии Unix, эти оболочки используют для решения проблемы целый набор инструментов, выполняющих небольшие частные задачи: ls, wc, sort, grep, diff, tar, ... Обычно простые задачи выполняются в командной оболочке запуcком соответствующих инструментов и организацией потока данных. Для более сложных задач требуются и более сложные инструменты, например, awk, sed или даже Perl. Так принято. Однако хотелось бы обратить внимание системных администраторов на такой сценарный (скриптовый) язык как Python. Этот язык, благодаря своим хорошим качествам, о которых поговорим далее, уверенно завоевывает популярность, в том числе для задач системного администрирования. Например, именно его применяет Red Hat в инструменте под названием anaconda для обеспечения начальной установки своего дистрибутива Linux. Конечно, системные администраторы — натуры весьма консервативные, и потому я решил написать эту статью, показывающую, что Python действительно имеет преимущества по сравнению с языком Perl и оболоч-

28

ками при решении как повседневных, так и одноразовых задач. Python — интерпретируемый язык с развитыми высокоуровневыми структурами данных, имеющий все необходимое для вызова функций POSIX-совместимых систем. Впрочем, Python является многоплатформенным языком, так что его можно с успехом использовать и, к примеру, в среде Windows. Однако не буду долго говорить о происхождении и синтаксисе языка: об этом заинтересованный читатель узнает на http://python.ru или из книги Сузи Р.А. Python. — СПб.: БХВ-Петербург, 2002; а сразу перейду к делу. Начнем с небольшого примера, в котором нам требуется установить права и принадлежность файлов, чтобы имена совпадали с именами пользователей в системе (для простоты будем считать, что имена пользователей доступны из файла /etc/passwd). Подобная задача может возникнуть, например, в каталоге с почтовыми ящиками, где каждый ящик должен принадлежать соответствующему его названию пользователю. #!/usr/bin/python import os, string users = {} for line in open("/etc/passwd").readlines(): rec = string.split(line, ":") users[rec[0]] = int(rec[2]), int(rec[3]) # uid è gid for file in os.listdir("."):


администрирование try: uid, gid = users[file] except: print "Ñèðîòà: ", file uid, gid = 0, 0 # root os.chmod(file, 0600) os.chown(file, uid, gid)

В самом начале мы импортируем модуль для работы с функциями ОС (os) и работы со строками (string). Перед началом цикла по строкам файла /etc/passwd словарь users пуст. В цикле по строкам файла /etc/passwd мы делаем следующее. Именем rec обозначаем кортеж значений записи passwd-файла. Мы знаем, что первое поле этой записи — имя пользователя. Имя пользователя и станет ключом в словаре users. В качестве значения для данного ключа мы берем приведенные к целому типу поля 2 и 3, соответствующие идентификаторам пользователя и группы. Таким образом в первом цикле формируется отображение имени пользователя и его идентификаторов. Во втором цикле, по именам файлов в текущем каталоге, пытаемся (try) найти владельца файла, обращаясь к словарю users. Если это не удается, мы пишем на стандартный вывод соответствующую диагностику. В этом случае владельцем файла станет root. Последние две команды, думается, ясны и без комментариев. В приведенном на листинге примере были использованы очевидные решения, не требующие особого знания стандартной библиотеки Python. Для любознательных укажем и второй путь решения, в котором используются «правильные» средства: #!/usr/bin/python import os, pwd, glob default = pwd.getpwnam("root") for file in glob.glob("*"): try: rec = pwd.getpwnam(file) except: print "Ñèðîòà: ", file rec = default os.chmod(file, 0600) os.chown(file, rec[2], rec[3])

Здесь удалось добиться некоторого сокращения программы за счет использования функции getpwnam модуля pwd стандартной библиотеки Python. Здесь также применена функция glob из одноименного модуля для получения списка файлов. Для сравнения приведен неправильный пример подобной же программы, написанный в оболочке bash. #!/bin/bash # Íå ðàáîòàåò, åñëè èìåíà ôàéëîâ ñîäåðæàò òî÷êó èëè ïðîáåë for file in * do chown $file.$file $file || chown root.root $file chmod 600 $file done

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

№1, октябрь 2002

файлов, содержащих в имени точку, пробел или дефис в начале! Я более чем уверен, что программу можно переписать, экранировав символы должным образом. Но тем не менее, зная Python, можно быстрее написать скрипт, решающий ту же задачу без скользких текстовых подстановок. Из приведенных примеров уже видна особенность языка Python, не всеми воспринимаемая хорошо: для выделения фрагментов кода в составных операторах используется единообразный отступ. Тем самым интерпретатор Python требует от программиста визуально выделять структуру программы, что положительно сказывается на читаемости кода. И это немаловажно, ведь написанный скрипт может пригодиться для похожей задачи. В следующем примере мы рассмотрим еще одну часто возникающую задачу: проверка работоспособности POP3-сервиса и отправка сообщения электронной почты в случае неудачи. Заметьте, что приведенный пример одинаково хорошо подходит и для Unix, и для NT. #!/usr/bin/python import smtplib, poplib try: p = poplib.POP3("mymail") p.quit() except: error = "connection" try: s = SMTP("othermail") s.sendmail("admin@mymail", "admin@othermail", """From: admin@mymail To: admin@othermail Subject: POP3 down!!! Please, restart pop3 at mymail. """)

s.quit() except: print "íåò ñâÿçè"

С помощью конструктора объекта POP3-соединения из модуля poplib мы пытаемся установить соединение с POP3-сервером mymail. Если это не удается, мы должны отправить о происшес твии письмо на admin@othermail. В этом нам поможет модуль smtplib и конструктор класса SMTP. Если и это не удается, просто выводится «нет связи». Заметьте, как легко в Python ввести многострочный текст. Надеемся, вы как минимум заинтересовались предлагаемым инструментом. Таблица 1 поможет быстро найти аналоги распространенных команд среди функций стандартной библиотеки языка Python. Перед использованием функции достаточно импортировать соответствующий модуль. Настало время разобрать более сложный пример -программу для анализа логов почтового сервера Sendmail. Наверное, не нужно долго объяснять, что для обеспечения бесперебойной работы сервера и требуемого качества обслуживания просто необходимо вовремя выявлять аномалии в его работе и пресекать по-

29


администрирование Таблица 1 °ÔÒÆÓÊÆ 8QL[ FDO ÒË×åÜ ÉÔÊ FG ÐÆØÆÑÔÉ FKPRG ÖËÌÎÒ ÕÙØâ FKRZQ XLG JLG ÕÙØâ FS ÚÆÏÑ ÕÙØâ GDWH GLII HFKR ÕËÖËÒ HFKR ØËÐ×Ø H[LW 1 IHWFKPDLO IWS JUHS HJUHS J]LS OQ V ÕÙØâ ÕÙØâ OQ ÚÆÏÑ ÚÆÏÑ OV O ÚÆÏÑ OV ÐÆØÆÑÔÉ PDQ ØËÒÆ PNGLU ÕÙØâ PY ÕÙØâ ÕÙØâ QVORRNXS ÛÔ×Ø SZG UP 5 ÐÆØÆÑÔÉ UP ÚÆÏÑ VHQGPDLO VRUW ZJHW Î Ø Õ ]LS ÐÔÒÆÓÊÆ ÐÔÒÆÓÊÆ _ _ ÐÔÒÆÓÊÆ ÚÆÏÑ ! ÚÆÏÑ

ºÙÓÐÜÎå 3\WKRQ FDOHQGDU PRQWKFDOHQGDU ÉÔÊ ÒË×åÜ RV FKGLU ÐÆØÆÑÔÉ RV FKPRG ÕÙØâ ÖËÌÎÒ RV FKRZQ ÕÙØâ XLG JLG VKXWLO FRS\ ÚÆÏÑ ÕÙØâ WLPH ORFDOWLPH WLPH WLPH

Î ÊÖÙÉÎË ÚÙÓÐÜÎÎ ÒÔÊÙÑå WLPH ÚÙÓÐÜÎÎ ÒÔÊÙÑå GLIIOLE SULQW ÕËÖËÒ V YDUV SULQW ØËÐ×Ø V\V H[LW 1 ÒÔÊÙÑâ SRSOLE ÒÔÊÙÑâ IWSOLE ÊÑå ÖÆÇÔØá × ÖËÉÙÑåÖÓáÒÎ ÈáÖÆÌËÓÎåÒÎ ×ÑÙÌÎØ ÒÔÊÙÑâ UH ÒÔÊÙÑâ J]LS RV V\POLQN ÚÆÏÑ ÚÆÏÑ RV OLQN ÚÆÏÑ ÚÆÏÑ RV VWDW ÚÆÏÑ RV OLVWGLU ÐÆØÆÑÔÉ JORE JORE ÐÆØÆÑÔÉ KHOS ØËÒÆ ÎÑÎ KHOS ÔÇàËÐØ RV PNGLU ÕÙØâ RV UHQDPH ÕÙØâ ÕÙØâ VRFNHW JHWKRVWE\QDPH ÛÔ×Ø Î ÊÖÙÉÎË ÚÙÓÐÜÎÎ ãØÔÉÔ ÒÔÊÙÑå RV JHWFZG VKXWLO UPWUHH ÐÆØÆÑÔÉ RV XQOLQN ÚÆÏÑ ÎÑÎ RV UHPRYH ÚÆÏÑ ÒÔÊÙÑâ VPWSOLE ×ÕÎ×ÔÐ VRUW ÒÔÊÙÑÎ KWWSOLE XUOOLE XUOOLE XUOSDUVH ÒÔÊÙÑâ ]LSILOH RV V\VWHP ÐÔÒÆÓÊÆ I RV SRSHQ ÐÔÒÆÓÊÆ U I RV SRSHQ ÐÔÒÆÓÊÆ Z I RSHQ ÚÆÏÑ U I UHDG I FORVH I RSHQ ÚÆÏÑ U I ZULWH I FORVH

пытки злоупотребления сервисом. Одна из основных проблем - массовые несанкционированные рассылки, или попросту спам. И проблема эта не только в том, чтобы уберечь пользователей сервера от спама, но и вовремя заметить попытки применения пользователями программ массовой рассылки. Я почти уверен, что на рынке имеются готовые инструменты для решения этой проблемы. Даже очень возможно, что нужные программы есть в свободном распространении. Однако простые средства во многих случаях можно написать и самому, руководствуясь своим опытом и, возможно, знанием местной специфики. Тем более, что это не требует много времени. Разбираемый ниже пример не претендует на полноту решения проблемы спама, он решает частную задачу. Мы будем отслеживать лишь попытки массовых рассылок, в которых на один «mail from» следуют несколько «rcpt to». Конечно, программу легко адаптировать и для другой ситуации, агрегируя данные по отправителям, получателям и так далее, получая портрет ситуации на почтовом сервере. Следует заметить, что глядя

30


администрирование на «сырые» логи Sendmail не всегда можно увидеть проблему. Во всяком случае, для этого требуется некоторое напряжение глаз. Наша небольшая программа на Python будет собирать данные о сеансах и показывать только сеансы с большим числом получателей. В логе эта информация сильно размазана, поэтому простого применения инструмента вроде grep здесь оказывается недостаточно. Накапливаемые данные мы будем сохранять в файлах-хэшах (аналогичных тем, в которых хранятся, например, псевдонимы), благо в Python есть для этого нужные модули. (Конечно, в Python можно использовать и полновесные базы данных с языком запросов SQL, однако это непринципиально). Задача распадается на две: сбор данных из лога в базу данных и выборка из базы данных. #!/usr/bin/python """Àíàëèç ëîãà Sendmail""" import os, re, sys, anydbm, time server_name = "mail"

# èìÿ ñåðâåðà, ôèãóðèðóþùåå â ëîãàõ

def createdb(file): """Ñîçäàíèå ÁÄ""" try: os.unlink(file) # óäàëÿåì ñòàðóþ ÁÄ except: pass # èãíîðèðóåì èñêëþ÷åíèÿ return anydbm.open(file, "c") # êëþ÷: çíà÷åíèå f = createdb("from.db") # èä ñåññèè: îòïðàâèòåëü t = createdb("to.db") # èä ñåññèè: ïîëó÷àòåëè r = createdb("relay.db") # èä ñåññèè: ïî÷òîâûé õîñò d = createdb("date.db") # âðåìÿ è èä ñåññèè: èä ñåññèè ''' Ïðèìåðû ÷åòûðåõ õàðàêòåðíûõ ñëó÷àåâ: Aug 11 20:24:25 mail sendmail[4720]: g7BGOHY2004720: from=<tegeran@mail.ru>, size=103655, class=0, nrcpts=1, msgid=<000c01c2414e$d175ad80$3075a8c0@awx.ru>, proto=SMTP, daemon=MTA, relay=[212.28.127.12] Aug 11 20:24:25 mail sendmail[4720]: g7BGOHY2004720: to=<font@twogo.ru>, delay=00:00:07, pri=133655, stat=Headers too large (32768 max)''' Aug 11 04:02:31 mail sendmail[17058]: g7B02UY2017058: from=<gluck@subscribe.ru>, size=33977, class=0, nrcpts=1, msgid=<20020811030027_hk_=2087=top=n=n_@subscribe:news.listsoft.lnx>, proto=SMTP, daemon=MTA, relay=relay1.aport.ru [194.67.18.127] Aug 11 07:18:20 mail sendmail[28329]: g7B3ICY2028329: <nouser@twogo.ru>... User unknown'''

date, session_id, addr = found["date"], found["session"], found["addr"] direc = found.get("direc", "to") # ïî óìîë÷àíèþ "to" stat = found.get("stat", "") # ïî óìîë÷àíèþ - ïóñòàÿ ñòðîêà if direc == "to" and stat[:4] != "Sent" and stat != "User unknown": continue # òàêèå ñîîáùåíèÿ íå èíòåðåñóþò if direc == "to": # â çàâèñèìîñòè îò íàïðàâëåíèÿ if t.has_key(session_id): t[session_id] = t[session_id] + addr + ";" else: t[session_id] = addr + ";" else: f[session_id] = addr r[session_id] = found.get("relay_ip", "") or found.get("relay", "") d[date + session_id] = session_id input_file.close() f.close(); t.close(); r.close(); d.close()

Построчно читаем из файла с логами и, в зависимости от сработавшего шаблона, записываем в заранее открытые базы данных. Обратите внимание, что база date.db использует в качестве ключей как дату, так и идентификатор сессии. Последний делает ключ уникальным. Заметим, что в Python логические операции or и and работают несколько необычно. Любой объект имеет так называемое истинностное значение. Нули, пустые последовательности, объект None считаются «ложью», а остальные объекты -- истиной. В результате операции A or B возвращается объект A, если он «истинен», а в противном случае -- объект B. Именно поэтому имени m будет соответствовать результат первого успешного сравнения. Python оперирует такими высокоуровневыми объектами, как список и словарь. Словарь задает отображение между ключами и значениями. В частности, found является словарем, отображающим имя найденной в строке лога группы и значения этой группы. Имена групп

# õàðàêòåðíûå ÷àñòè ðåãóëÿðíûõ âûðàæåíèé (ïðåôèêñ, õîñò è àäðåñ) P = "(?P<date>.{15}) %(server_name)s sendmail\[[0-9]+\]: (?P<session>[^:]+): " % vars() R = "relay=(?P<relay>.*(?:\[(?P<relay_ip>.+?)\])?)" A = "\<?(?P<addr>[^(),>]+)\>?" # ðåãóëÿðíûå âûðàæåíèÿ, ñîîòâåòñòâóþùèå ðàçíûì ñëó÷àÿì log1_re = re.compile(r"%(P)s(?P<direc>to)=%(A)s, (.*), stat=(?P<stat>.*)" % vars()) log2_re = re.compile(r"%(P)s(?P<direc>from)=%(A)s, size=(?P<size>[0-9]+), .*, %(R)s" % vars()) log3_re = re.compile(r"%(P)s(?P<direc>from)=%(A)s, .*, %(R)s" % vars()) log4_re = re.compile(r"%(P)s%(A)s\.\.\. (?P<stat>User unknown)" % vars()) input_file = open(sys.argv[1], "r") # îòêðûâàåì ôàéë ñ ëîãîì while 1: line = input_file.readline() if not line: # îáíàðóæåí êîíåö ôàéëà: âûõîä èç öèêëà break # ñðàâíèâàåì ñòðîêó ëîãà ñ øàáëîíàìè m = log1_re.match(line) or log2_re.match(line) \ or log3_re.match(line) or log4_re.match(line) if m: # åñëè õîòü îäèí øàáëîí ñðàáîòàë: found = m.groupdict() # ïîëó÷àåì ñëîâàðü ãðóïï ðåçóëüòàòà

№1, октябрь 2002

31


администрирование в регулярных выражениях задаются через (?P<имя> ...). Взять из словаря значение по ключу можно не только с помощью словарь[ключ], но и с помощью метода get(). В последнем случае можно задать значение «по умолчанию», то есть значение, которое метод get() возвратит в случае отсутствия ключа в словаре. Стоит заметить, что работа с базами данных использует тот же синтаксис, что и работа со словарями (об этом позаботились разработчики модуля anydbm). Итак, мы поместили информацию из лога Sendmail в более удобный для обработки вид -- в базы данных (хэши). Следующая программа - пример одного из обработчиков, которые мы теперь можем применить. Для нашей цели (выявление чрезмерных списков рассылки) данные удобно отсортировать по времени. Это легко сделать с ключами из базы date.db. По сути мы считываем все ключи этой базы в память (метод keys()) и сортируем их по возрастанию. #!/usr/bin/python """Àíàëèçèðóåì ñîáðàííûå äàííûå""" import re, sys, anydbm, string # f t r d

îòêðûâàåì áàçû = anydbm.open("from.db", "r") = anydbm.open("to.db", "r") = anydbm.open("relay.db", "r") = anydbm.open("date.db", "r")

# ïîêàçûâàòü òîëüêî ñëó÷àè îòïðàâëåíèÿ >= LIM ïîëó÷àòåëÿì try: LIM = int(sys.argv[1]) except: LIM = 4 dkeys = d.keys() # ïîëó÷àåì âñå êëþ÷è â îäèí ñïèñîê # ñîðòèðóåì (ôàêòè÷åñêè, ïî âðåìåíè). Ïðåíåáðåãàåì ñìåíîé ìåñÿöåâ dkeys.sort() good_guys_re = re.compile(".*(gluck@subscribe.ru|errors@maillist.ru|@lists.cityline.ru|@host4.list.ru)\Z") def our_filter(x): """Ôóíêöèÿ äëÿ ôèëüòðàöèè ïîëó÷àòåëåé""" return "@" in x for date_sess_id in dkeys: i = d[date_sess_id] # íàõîäèì èäåíòèôèêàòîð ñåññèè try: recps = string.split(t[i], ";") # ñïèñîê ïîëó÷àòåëåé # áåðåì òîëüêî àäðåñà ñ @ recps = filter(our_filter, recps) sender = f[i] # îòïðàâèòåëü # ïîêàçûâàåì if len(recps) >= LIM and not good_guys_re.match(sender): relay = r[i] dte = date_sess_id[:15] # ïåðâûå 15 ñèìâîëîâ -- äàòà print f[i], relay, dte, "->\n ", string.join(recps, "\n ") except: # åñëè âîçíèêëè îøèáêè, ïðîïóñêàåì pass f.close(); t.close(); r.close()

Здесь особо следует отметить функцию filter(). Эта встроенная функция применяет некоторую функцию (аргумент 1) к списку (аргумент 2). В нашем случае используется our_filter, которая возвращает «истину» в случае, когда ее аргумент содержит «@». Составленная программа далека от совершенства, однако ее легко доработать в любом нужном направлении: добавить возможность обработки нескольких

32

файлов логов сразу; обрабатывать ситуации, когда несколько получателей указаны в одной строке лога; изменить правила фильтрации, изменить способ агрегирования данных (например, агрегировать по именам получателей, именам отправителей, адресам почтовых хостов); сделать из программы демона, непрерывно следящего за логами и т.п. Составляя наши программы, мы выразили логику анализа логов почтового сервера и почти не отвлекались на обдумывание применяемых конструкций языка программирования и конструирование типов данных. Программисты на Python утверждают, что на этом языке можно писать «со скоростью мысли», то есть, почти все время оставаясь в предметной области. Если вы этому не верите, попробуйте переписать приведенные программы с использованием стандартного Си или Java. С другой стороны, приведенную задачу обычно решают с помощью языка Perl. К сожалению, Perl вносит очень много синтаксического мусора, что подчас значительно затеняет логику программы. Основными преимуществами Python являются, на мой взгляд:

n удобный и легко читаемый синтаксис n многоплатформенность n поддержка основных системных и сетевых протоколов и форматов

n большой набор библиотек n свободное распространение n дружелюбная поддержка в телеконференции comp.lang.python

n надежный и устойчивый интерпретатор Все это делает Python отличным инструментом в работе системного администратора.


ПРОГРАММИРОВАНИЕ


ЕВГЕНИЙ КОНОВАЛОВ

РАБОТА С ТЕКСТОМ ИЛИ ФИЛОСОФИЯ PERL


программирование Есть два основных аспекта, которые легли в основу этой статьи. Первый - это периодически возникающая перед большинством Web-программистов проблема переноса тех или иных документов в базу данных (БД), используемую, например, при генерации страниц скриптами. Проблема эта из разряда концептуальных и решается по-разному: в зависимости от рабочей среды, окружающей разработчика плотным кольцом всевозможных требований, условий и согласований. Вторая идея, попавшая в фокус обсуждения использование Perl для решения любопытной задачи, связанной с упомянутой выше проблемой. Собственно об этом и пойдет речь в дальнейшем. (Примечание: данная статья, пожалуй, рассматривается именно как посылка для обсуждения - иные подходы не имеют и половины такой же привлекательности.)

Вместо лирического вступления На всякий случай автор искренне просит не рассматривать эту статью как очередное «УРА» в честь наиболее развитого и удобного, свободного, попросту приятного и даже замечательного языка для работы с текстом и программирования в Web (разумеется, речь идет о Perl).

Сильным духом программистам посвящается... «Если ты едешь тише - дольше ехать тебе. Если поспешишь, то не разглядишь идей»

Следуя этой нехитрой идее, хочется без излишних подробностей, но, тем не менее, вдумчиво изложить уважаемому читателю идеи, которые, вполне возможно, пригодятся в работе или просто послужат поводом для размышлений. Сначала несколько слов об истоках рассматриваемой проблемы. Сегодня адаптация документов различных форматов для внесения их в БД Web-сайта - занятие привычное во многих организациях, имеющих собственную «точку присутствия» в сети Интернет. Зачастую это связано с возможностью простой реализации полнотекстового поиска в таблицах БД и простотой реализации механизмадоступа к информации. Так или иначе, все выглядит достаточно просто и понятно, когда речь заходит о небольших объемах данных, особенно, если они подготавливаются централизовано одним-двумя сотрудниками. Сложности возникают при работе с документами больших размеров.{1} Что также может осложняться острым желанием предоставить администратору БД и многочисленным контент-менеджерам удобную методику для работы с данными. Одновременно специфика Web, конечно же, делает предпочтительной разбивку крупных документов на отдельные составляющие (например, главы), и предостав-

№1, октябрь 2002

ление пользователю возможности их раздельного просмотра. Это связано с сегодняшней философией жизни в Интернет, например, с «диалапными» скоростями доступа (впрочем, не только с этим). Подытоживая сказанное, Вашему вниманию представляется задача, решенная в рамках разработки подсистемы информационного наполнения БД. Исходные данные: n тип вносимой информации - HTML-код значительного объема; n весь HTML-код заключен в едином документе; n документ необходимо разбить на составные части, которые размещаются в БД (с тем же успехом их можно сохранить в файл). Основная трудность решения: в результате разбиения документа необходимо получить фрагменты HTML-кода, имеющие корректную HTML-структуру. Такого рода корректность подразумевает: n соответствие каждому начальному HTML-тегу завершающего HTML-тега (для HTML-элементов, обязательно обозначаемых именно парой тегов); n переработку внутренних гиперссылок (для случаев, когда сама ссылка оказывается в одном фрагменте, а то место в оригинальном документе, на которое она ссылается, - в другом фрагменте).

Отправная точка: исходные данные в подробностях «Начинать нужно с чего-то...», - и с этим трудно не согласиться.

Чтобы стало понятным все, о чем пойдет речь в дальнейшем, необходимо внести большую ясность в то, что касается исходных данных. На вход описанного ниже программного модуля, поступают должным образом подготовленные оглавление и текст HTML-документа. Далее приведено описание правил, по которым осуществляется такого рода подготовка документа. Говоря коротко, необходимо иметь HTML-документ с гипертекстовым оглавлением (в оглавлении должны использоваться внутренние гиперссылки - то есть те, значения атрибутов href которых начинаются со знака решетки «#»). Такой подход достаточно удобен. Теперь о том же самом, но уже более подробно. Дабы исключить путаницу в терминах введем несколько понятий. «Внутренняя гиперссылка» - ссылка, при нажатии на которую выполняется переход на анкер (см. ниже), расположенный в том же HTML-документе, в теле которого находится и сама ссылка; значение атрибута HREF такой гиперссылки начинается со знака решетки #. Например: <A HREF=»#Part1">1. Introduction</A>.

«Анкер» - тег, размещаемый в том месте HTML-документа, куда необходимо осуществить перемещение по нажатию на внутреннюю гиперссылку. Например: <A NAME=»Part1">

35


программирование Примечание: связь между анкером и внутренней гиперссылкой реализуется через значения атрибута HREF гиперссылки и атрибута NAME анкера, при этом значение NAME соответствует значению HREF с точностью до знака решетки. Например: <A HREF=»#Part1">1. Introduction</A>

является внутренней ссылкой на анкер <A NAME=»Part1">

«Оглавление» - гипертекстовое оглавление документа, состоящее из внутренних гиперссылок. «Пункт оглавления» - внутренняя гиперссылка, внутри тега которой заключен текст одного из пунктов содержания документа. «Глава документа» или «Глава» - часть HTML-документа, сопоставляемая одному из пунктов оглавления. «Специальный анкер разметки» или «Анкер разметки» - анкер, на который ссылается один из пунктов оглавления. (Такой анкер разметки располагается непосредственно перед главой (обычно перед заглавием главы), которая соответствует пункту оглавления, ссылающемуся на данный анкер разметки.) «Документ» - текст HTML-документа (без оглавления). Далее, рассмотрим в качестве примера документа обзор по материнским платам. Вообще говоря, в рассматриваемом случае оглавление в традиционном понимании имеет следующий вид: 1. 2. 3. 4. 5.

Introduction CPUs and CPU Sockets Supported Memory types and speed. Chipset features Built-in video and audio cards Hard drive support. USB ports and extension slots

Назовем это оглавление «Исходным оглавлением». Однако для корректного разбиения документа на главы оглавление должно быть представлено, например, следующим образом: <A HREF=»#Part1">1. Introduction</A> <A HREF=»#Part2">2. CPUs and CPU Sockets</A> <A HREF=»#Part3">3. Supported Memory types and speed. Chipset features</A> <A HREF=»#Part4">4. Built-in video and audio cards</A> <A HREF=»#Part5">5. Hard drive support. USB ports and extension slots</A>

В данном представлении обязательным является использование для каждого пункта исходного оглавления (см. выше) синтаксической конструкции вида: <A HREF=»#Unique_ID»>Òåêñò ïóíêòà îãëàâëåíèÿ</A>

где Unique_ID - произвольный идентификатор, который должен быть уникальным применительно ко всем значениям атрибутов HREF всех пунктов оглавления. Ниже приведен соответствующий документ - это фрагмент HTML-кода из пяти глав (содержательная часть, разумеется, беспощадно урезана - буквально до потери смысла).

36

Примечание: при программном анализе документа заголовочная часть (вплоть до тега <BODY>) и концевые теги </HTML> и </BODY> не рассматриваются. <HTML> <HEADER>Motherboard Description</HEADER> <BODY> <A NAME=»Part1"></A> <TABLE> <TR><TD> <B><H3>Introduction.</H3></B><BR><BR ><FONT COLOR=»#000099">Mainboard is the most vital component of every PC. If we compare a PC with a living organism, the processor can be compared to a heart, and the mainboard is like a system of artherias and veins which deliver information from a processor to other parts of PC.</FONT> <A NAME=»Part2"></A> <P><B><H3>CPUs and CPU Sockets.</H3></B> <P><FONT COLOR=»#000099">So, it is time for you to buy a new PC. We will not discuss the modern processors in this section, as it is revealed in our </FONT><B><FONT COLOR=»#CC0000">processors section</FONT></B><FONT COLOR=»#000099">, but will concentrate on the parts responsible to connecting CPU (Central Processor Unit) to the mainboard, the sockets, since there is a great variety of them nowadays.</ FONT> <A NAME=»Part3"></A> <P><B><H3>Supported Memory types and speed. Chipset features.</ H3></B> <P><FONT COLOR=»#000099">Not only the CPU is responsible for overall performance of your desktop (or notebook) system. Even the extremely speedy processor will be found slowed by less performing RAM (Random Ac ss Memory).</FONT> <A NAME=»Part4"></A><P> <P><B><H3>Built-in video and audio cards.</H3></B> <P><FONT COLOR=»#000099">As you see, the latest chipsets have built-in audio cardsnd video adapters. <A NAME=»Part5"></A> <P><B><H3>Hard drive support. USB ports and extension slots.</ H3></B> <P><FONT COLOR=»#000099">The times when you had to install multiple intput-output cards can be forgotten and now you have all drive (hard and floppy) support on the mainboard. </TD></TR> </TABLE> </BODY> </HTML>

Замечание 1: в тексте документа не может быть более одного анкера разметки с одним и тем же значением атрибута «NAME» (при этом количество анкеров разметки равно числу пунктов оглавления). Замечание 2: очевидно, что при разбиении на фрагменты, первая глава будет содержать стартовые теги <TABLE>, <TR> и <TD> без концевых (завершающих) тегов. Это явное нарушение HTML-структуры, которое может быть особенно неприятным при использовании SSI (если HTML-код главы будет включаться в состав основной HTML-страницы средствами Web-сервера).

Программирование: идея, заложенная в основу «Идея - базис алгоритма», - и так повсюду.

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


программирование данных (см. далее), позволяющих описать документ с точки зрения типов тегов, содержащихся в нем, и их количества. На основе такой количественной статистики производится анализ корректности HTML-структуры всего документа и глав, на которые он будет разделен в дальнейшем. Идея проста: анализ сводится к подсчету количества стартовых и завершающих тегов и сравнении полученных величин, исправление обнаруженных ошибок заключается в добавлении недостающих стартовых и завершающих тегов. Остановимся на этом чуть подробнее. Суть заключается в том, что первоначально для каждой главы из тех, на которые разбивается документ подсчитывается число начальных и завершающих тегов (назовем их «Величина-1» и «Величина-2»). Затем при обнаружении несоответствия между этими значениями начинается итерационный поиск недостающего парного тега: последовательно перебираются последующие главы и для каждой такой главы к величине-1 и величине-2 соответственно прибавляются числа начальных и завершающих тегов этой главы. Условием успешного окончания поиска недостающего парного тега является равенство величины-1 и величины-2. Если назвать главу, в которой обнаружилось несоответствие, стартовой (Главой итерационного поиска); главу в которой был найден недостающий парный концевой тег заключительной, а все главы между ними - промежуточными, то процесс коррекции HTML-структуры глав можно описать достаточно просто. Необходимо дополнить стартовую главу концевым HTML-тегом, заключительную главу - стартовым HTML-тегом, промежуточные главы - стартовым и концевым HTML-тегами.

$attrseq, $origtext) и end($tag, $origtext). Первый из них вызывается при обнаружении начального HTML-тега, передаваемые ему параметры: $tag - имя тега, $attr - хэш с именем атрибута тега в качестве ключа и значением атрибута в качестве значения хэша, $attrseq - массив имен атрибутов тега, $origtext - HTML-код тега. Второй метод выполняется при обнаружении завершающего HTML-тега: $tag - имя тега, $origtext - HTML-код тега. В программе методы start и end перегружаются (замещаются). Именно в рамках этих процедур производится потеговый анализ документа и собирается упомянутая выше статистика, характеризующая его HTML-структуру.

Программирование: основные структуры данных «Структура структурной структуризации структурирует структурную типизацию», - и все это не случайно.

Речь идет о структурах данных объектов класса Manual_Parser - в них накапливается статистика, позволяющая проанализировать HTML-структуру документа. Существует три основные структуры данных: $Doc_Info, $Work_Container и массив @Pages_Info. Сразу же следует заметить, что речь идет о сложных, вложенных структурах данных. Их понимание играет ключевую роль. Каждый элемент массива @Pages_Info представляет собой структуру данных, с элементами в виде скаляров, списков и хэшей, описывающих HTML-структуру одной главы (замечание в рамках борьбы с несозвучностью названий и терминологии: под «Page» понимается HTMLстраница, соответствующая главе).

Программирование: иерархия классов При работе упор был сделан на методику объектноориентированного программирования. Именно это позволило использовать уже существующие классы HTML::Parser, предоставляющий методы для разбора SGML-документов (так же используется HTML::Filter - дочерний класс HTML::Parser). Было разработано два класса: Manual_Cont_Parser и Manual_Parser. Manual_Cont_Parser наследуется от HTML::Filter, класс занимается обработкой оглавления (см. выше), разбирая его попунктно. Предоставляет методы: Get_Links_Lines - возвращает массив, каждый элемент которого содержит текст пункта оглавления (без тегов); Get_Links_Hrefs - возвращает массив, каждый элемент которого содержит значение атрибута HREF пункта оглавления. Manual_Parser наследуется от HTML::Parser. Отвечает за разбор документа и его разбиение на главы. Именно этому классу и будет уделено основное внимание. Класс HTML::Parser обладает рядом методов. Ключевыми являются методы parse($string) или parse_file($file), вызов одного из них инициирует работу по анализу документа. Еще два используемых метода - start($tag, $attr,

№1, октябрь 2002

37


программирование @Pages_Info = { #Õýø ñòðóêòóð (ñì. íèæå) Tags => {%Tags}, #Õýø ñòðóêòóð (ñì. íèæå) Tags_Seq_Page => {%Tags_Seq_Page}, #Îáùåå êîëè÷åñòâî ñòàðòîâûõ òåãîâ â ïðåäåëàõ îäíîé ãëàâû Num_Of_Tags => $Num_Of_Tags, #Ìàññèâ, êàæäîå çíà÷åíèå - ïîðÿäêîâûé íîìåð òåãà â Äîêóìåíòå, äëÿ êîòîðîãî â Ãëàâå íåîáõîäèìî äîáàâèòü ñòàðòîâûé òåã. Start_Tags_To_Add => {@Start_Tags_To_Add}, #Ìàññèâ, êàæäîå çíà÷åíèå - ïîðÿäêîâûé íîìåð òåãà â Äîêóìåíòå, äëÿ êîòîðîãî â Ãëàâå íåîáõîäèìî äîáàâèòü çàâåðøàþùèé òåã. End_Tags_To_Add => {@End_Tags_To_Add}, #Õýø, êëþ÷ õýøà - çíà÷åíèå àòðèáóòà href Âíóòðåííåé Ãèïåðññûëêè, çíà÷åíèå õýøà - íîìåð Ãëàâû Local_Links => %Local_Links, #Õýø, êëþ÷ õýøà - çíà÷åíèå àòðèáóòà href Âíóòðåííåé Ãèïåðññûëêè íà îäíó èç Ãëàâ, çíà÷åíèå õýøà - íîìåð Ãëàâû Local_Links_To_Pages => %Local_Links_To_Pages }; #Õýø ñòðóêòóð, êëþ÷ õýøà - ïîðÿäêîâûé íîìåð òåãà â òåêóùåé Ãëàâå %Tags_Seq_Page = { #Èìÿ òåãà Tag_Name => $Tag_Name, #Ïîðÿäêîâûé íîìåð òåãà âî âñåì Äîêóìåíòå Tag_Num_In_Doc => $Tag_Num_In_Doc }; #Õýø ñòðóêòóð, êëþ÷ õýøà - èìÿ (òèï) òåãà %Tags = { #Êîëè÷åñòâî íà÷àëüíûõ HTML-òåãîâ äàííîãî òèïà â Ãëàâå Start_Tag_Num => $Start_Tag_Num, #Êîëè÷åñòâî çàêëþ÷èòåëüíûõ HTML-òåãîâ äàííîãî òèïà â Ãëàâå End_Tag_Num => $End_Tag_Num };

$Doc_Info представляет собой структуру данных, с элементами в виде скаляров, списков и хэшей, описывающих документ вцелом. $Doc_Info = { #Õýø ñòðóêòóð (ñì. íèæå) Tags_Seq_Doc => {%Tags_Seq_Doc}, #Õýø ñòðóêòóð (ñì. íèæå) Tags => {%Tags}, #Îáùåå êîëè÷åñòâî ñòàðòîâûõ òåãîâ â ïðåäåëàõ Äîêóìåíòà All_Start_Tags_Num => $All_Start_Tags_Num, #Îáùåå êîëè÷åñòâî çàâåðøàþùèõ òåãîâ â ïðåäåëàõ Äîêóìåíòà All_End_Tags_Num => $All_End_Tags_Num, #Õåø, êëþ÷ õýøà - èìÿ àíêåðà, çíà÷åíèå õåøà - íîìåð Ãëàâû, â êîòîðîé ðàñïîëîæåí àíêåð Local_Anchors => %Local_Anchors }; #Õýø ñòðóêòóð, êëþ÷ õýøà - ïîðÿäêîâûé íîìåð òåãà â Äîêóìåíòå %Tags_Seq_Doc = { #Èìÿ òåãà Tag_Name => $Tag_Name, #HTML-êîä òåãà Tag_Orig_Text => $Tag_Orig_Text, #Íîìåð òåãà â Ãëàâå Tag_Num_In_Page => $Tag_Num_In_Page, #Íîìåð Ãëàâû, â êîòîðîé íàéäåí äàííûé òåã Tag_Page_Num => $Tag_Page_Num };

$Work_Container - структура, представляющая собой контейнер «рабочих» данных, она используется для временного хранения данных в процессе анализа. $Work_Container { #Â ýòó ïåðåìåííóþ çàíîñèòñÿ íîìåð Ãëàâû, â êîòîðîé íåäîñòàåò çàâåðøàþùåãî òåãà. Ãëàâà ñòàíîâèòñÿ ñòàðòîâîé äëÿ èòåðàöèîííîãî ïîèñêà òîé Ãëàâû, â êîòîðîé ñîäåðæèòñÿ íåäîñòàþùèé çàâåðøàþùèé òåã Start_Page_Num => $Start_Page_Num,

38

#Èìÿ àíàëèçèðóåìîãî òåãà Tag_Name => $Tag_Name, #Íîìåð àíàëèçèðóåìîãî òåãà â Äîêóìåíòå Tag_Num_In_Doc => $Tag_Num_In_Doc, #Íîìåð àíàëèçèðóåìîãî òåãà â Ãëàâå Tag_Num_In_Page => $Tag_Num_In_Page, #Ïåðåìåííàÿ äëÿ ïîäñ÷åòà îáùåãî êîëè÷åñòâà ñòàðòîâûõ HTMLòåãîâ â Äîêóìåíòå Start_Tag_Num_Total => $Start_Tag_Num_Total, #Ïåðåìåííàÿ äëÿ ïîäñ÷åòà îáùåãî êîëè÷åñòâà çàâåðøàþùèõ HTMLòåãîâ â Äîêóìåíòå End_Tag_Num_Total => $End_Tag_Num_Total };

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

Ниже приведены прокомментированные строки кода, указываемые в приложении, использующем классы Manual_Cont_Parser и Manual_Parser. Создание объекта класса Manual_Cont_Parser: my $Man_Cont_Parser = Manual_Cont_Parser->new();

Подготовка к анализу документа: разбор оглавления документа, получение списка анкеров разметки: $Man_Cont_Parser->parse($contents); my @Contents_Hrefs = $Man_Cont_Parser->Get_Links_Hrefs;

Создание объекта класса Manual_Parser: $Man_Parser->parse($Document_Text);

Анализ документа, разбор, корректировка и разбиение на главы: my $Man_Parser = Manual_Parser->new(\@Contents_Hrefs);

Использование полученных результатов: внесение полученных глав в БД Web-подсистемы: $Man_Parser->Insert_Pages_In_DB();

Подготовка к анализу HTML-документа Первоначально объекту класса Manual _Cont_Parser передается оглавление. Вызываются методы parse($text) или parse_file($file). Затем с помощью метода Get_Links_Hrefs получается результирующий список строк, каждая из которых содержит преобразованное значение атрибута href одного из пунктов оглавления (преобразование заключается в удалении символа «#», с которого начинается значение данного атрибута). Затем строки полученного списка будут использоваться объектом класса Manual_Parser для нахождения анкеров разметки в документе.


программирование Анализ HTML-документа: тег за тегом Далее подробно комментируется программная реализация решения описанной выше задачи. Приведенный наже программный код относится к классу Manual_Parser. @Array - массив (инициализируется в конструкторе при создании объекта), в котором содержатся имена анкеров. Разметки, полученные в виде результирующего списка на стадии подготовки к анализу документа (см. ранее). Процедура start() - замещает процедуру start() класса HTML::Parser, вызывается при нахождении стартового (начального) HTML-тега. Процедура работает со всем текстом документа, «физического» разбиения на главы на данном этапе не произодится - каждая глава рассматривается как составная часть документа. sub start { my $self = shift; #Ïîëó÷åíèå ïàðàìåòðîâ òåãà (îïèñàíèå ñì. ðàíåå) my ($tag, $attr, $attrseq, $origtext) = @_; my

@Temp_Cont_HREFs = @{$self->{Contents_HREFs}};

#Ïðîâåðêà: âõîäèò ëè íàéäåííûé ñòàðòîâûé òåã â ñïèñîê òåõ òåãîâ, äëÿ êîòîðûõ íå îáÿçàòåëüíî óêàçûâàòü ïàðíûé êîíöåâîé òåã if (!($self->Tag_Is_Exception_Check($tag))) { #Ïðîâåðêà: ÿâëÿåòñÿ ëè íàéäåííûé òåã ãèïåðññûëêîé èëè àíêåðîì if ($tag eq a ) { my $Last_Tag_Is_Cont_Href=0; #Äëÿ êàæäîãî èìåíè Àíêåðà Ðàçìåòêè foreach $Temp (@Array) { #Ïðîâåðêà: íàéäåííûé òåã - Àíêåð Ðàçìåòêè if ($attr->{ name } eq $Temp && $attr->{ name } ne ) Âûñòàâëåíèå ôëàãîâ, èçìåíåíèÿ ñ÷åò÷èêîâ {

#Ïðîâåðêà: íàéäåííûé òåã - Âíóòðåííÿÿ Ãèïåðññûëêà if ($attr->{ href } =~ /^#/ ) { my $Temp2 = $attr->{ href }; $Temp2 =~ s/#//; $self->{Pages_Info}[$self->{Current_Page}]>{Local_Links}{$Temp2}=$self->{Current_Page};

}

} }

#Îáíîâëåíèå ñòàòèñòèêè ïî íàéäåííîìó òåãó â ñòðóêòóðå äàííûõ ïî òåêóùåé Ãëàâå $self->{Pages_Info}[$self->{Current_Page}]>{Num_Of_Tags}++; $self->{Pages_Info}[$self->{Current_Page}]->{Tags}{«$tag»}>{Start_Tag_Num}++; $self->{Pages_Info}[$self->{Current_Page}]>{Tags_Seq_Page}{$self->{Tag_Num_In_Curr_Page}}>{Tag_Name}=$tag; $self->{Pages_Info}[$self->{Current_Page}]>{Tags_Seq_Page}{$self->{Tag_Num_In_Curr_Page}}>{Tag_Num_In_Doc} = $self->{Tag_Num_In_Doc}; #Îáíîâëåíèå ñòàòèñòèêè ïî íàéäåííîìó òåãó â ñòðóêòóðå äàííûõ ïî Äîêóìåíòó $self->{Doc_Info}->{Tags}{«$tag»}->{Start_Tag_Num}++; $self->{Doc_Info}->{Tags_Seq_Doc}{$self->{Tag_Num_In_Doc}}>{Tag_Name}=$tag; $self->{Doc_Info}->{Tags_Seq_Doc}{$self->{Tag_Num_In_Doc}}>{Tag_Orig_Text}=$origtext; $self->{Doc_Info}->{Tags_Seq_Doc}{$self->{Tag_Num_In_Doc}}>{Tag_Num_In_Page}=$self->{Tag_Num_In_Curr_Page}; $self->{Doc_Info}->{Tags_Seq_Doc}{$self->{Tag_Num_In_Doc}}>{Tag_Page_Num}=$self->{Current_Page}; $self->{Doc_Info}->{All_Start_Tags_Num}++; $self->{Tag_Num_In_Curr_Page}++; #Ñ÷åò÷èê ÷èñëà òåãîâ âäîêóìåíòå (èíèöèàëèçèðóåòñÿ â êîíñòðóêòîðå ïðè ñîçäàíèè îáúåêòà) $self->{Tag_Num_In_Doc}++; } }

#Ñ÷åò÷èê ÷èñëà òåãîâ â Ãëàâå (èíèöèàëèçèðóåòñÿ â êîíñòðóêòîðå ïðè ñîçäàíèè îáúåêòà) - çíà÷åíèå ðàâíî ïîðÿäêîâîìó íîìåðó (â Ãëàâå) àíàëèçèðóåìîãî òåãà $self->{Tag_Num_In_Curr_Page}=0; #Ñ÷åò÷èê ÷èñëà ñòðàíèö (èíèöèàëèçèðóåòñÿ â êîíñòðóêòîðå ïðè ñîçäàíèè îáúåêòà) - çíà÷åíèå ðàâíî ïîðÿäêîâîìó íîìåðó òåêóùåé àíàëèçèðóåìîé Ãëàâû $self->{Current_Page}++; $Last_Tag_Is_Cont_Href=1; last; } #Ïðîâåðêà: íàéäåííûé òåã - ãèïåðññûëêà íà îäíó èç Ãëàâ elsif ($attr->{ href } eq join( , # ,$Temp)) { #Ñîõðàíèòü â ñòðóêòóðå äàííûå î íàéäåííîé Âíóòðåííåé Ãèïåðññûëêå íà îäíó èç Ãëàâ my $Temp2 = $attr->{ href }; $Temp2 =~ s/#//; $self->{Pages_Info}[$self->{Current_Page}]>{Local_Links_To_Pages}{$Temp2}=$self->{Current_Page}; $Last_Tag_Is_Cont_Href=1; } } #Ïðîâåðêà: ïîñëåäíèé íàéäåííûé òåã - íå Àíêåð Ðàçìåòêè è íå Âíóòðåííÿÿ Ãèïåðññûëêà íà îäíó èç Ãëàâ if ($Last_Tag_Is_Cont_Href==0) { #Ïðîâåðêà: íàéäåííûé òåã - àíêåð if ($attr->{ name } ne ) { $self->{Doc_Info}->{Local_Anchors}{$attr>{ name }}=$self->{Current_Page}; }

№1, октябрь 2002

39


программирование Процедура end() - замещает процедуру end() класса HTML::Parser, вызывается при нахождении стартового (начального) HTML-тега. Процедура работает со всем текстом документа, «физического» разбиения на главы на данном этапе не произодится - каждая глава рассматривается как составная часть документа. sub end { my $self = shift; #Ïîëó÷åíèå ïàðàìåòðîâ òåãà (îïèñàíèå ñì. ðàíåå) ($tag, $origtext)=@_; #Ïðîâåðêà: âõîäèò ëè íàéäåííûé ñòàðòîâûé òåã â ñïèñîê òåõ òåãîâ, äëÿ êîòîðûõ íå îáÿçàòåëüíî óêàçûâàòü ïàðíûé êîíöåâîé òåã if (!($self->Tag_Is_Exception_Check($tag))) { #Îáíîâëåíèå ñòàòèñòèêè ïî êîíöåâûì HTML-òåãàì äëÿ òåêóùåé Ãëàâû $self->{Pages_Info}[$self->{Current_Page}]->{Tags}{«$tag»}>{End_Tag_Num}++; #Îáíîâëåíèå ñòàòèñòèêè ïî êîíöåâûì HTML-òåãàì äëÿ âñåãî Äîêóìåíòà $self->{Doc_Info}->{Tags}{«$tag»}->{End_Tag_Num}++; $self->{Doc_Info}->{All_End_Tags_Num}++; } }

Процедура _Separate_Manual_Into_Pages() - используется для выборки глав из HTML-кода документа и сохранения их в массиве в виде отдельных текстовых фрагментов. При этом их HTML-структура не откорректирована. Для выборки требуемых фрагментов HTML-кода используются регулярные выражения. Синтаксис выражения зависит от того, какая именно глава должна быть выбрана (первая, последняя или любая из промежуточных). sub _Separate_Manual_Into_Pages { my $self = shift; my $Manual_Text = shift; my @Manual_Parts;

is;

for ($i=-1; $i<=$#Array; $i++) { #Ïðîâåðêà: âûáèðàåì èç Äîêóìåíòà ïåðâóþ Ãëàâó if($i==-1) { $Manual_Text =~ /(.*)<\s*a\s+name=»*$Array[$i+1]»*\s*>/

$Manual_Parts[$i+1]=$1; } #Ïðîâåðêà: âûáèðàåì èç Äîêóìåíòà ïðîìåæóòî÷íóþ Ãëàâó elsif ($i<$#Array) { $Manual_Text =~ /(<\s*a\s+name=»*$Array[$i]»*\s*>.*<\s*\/ a\s*>.*)<\s*a\s+name=»*$Array[$i+1]»*\s*>/is; $Manual_Parts[$i+1]=$1; } #Ïðîâåðêà: âûáèðàåì èç Äîêóìåíòà ïîñëåäíþþ Ãëàâó else { $Manual_Text =~ /(<\s*a\s+name=»*$Array[$i]»*\s*>.*<\s*\/ a\s*>.*)/is;

}

}

$Manual_Parts[$i+1]=$1;

#Âîçâðàùàåò ìàññèâ, êàæäûé ýëåìåíò êîòîðîãî ñîäåðæèò òåêñò îäíîé èç Ãëàâ return @Manual_Parts; }

40

Процедура Generate_Manual_Parts() - в данной процедуре реализован анализ статистики , собранной ранее по главам и документу. На основании анализа корректируется HTML-структура глав. sub Generate_Manual_Parts { my $self = shift; #Ïîëó÷åíèå Ãëàâ â âèäå îòäåëüíûõ ôðàãìåíòîâ HTML-êîäà è ñîõðàíåíèå èõ â ìàññèâ my @Manual_Parts=$self>_Separate_Manual_Into_Pages($Manual_Text); #Èíèöèàëèçàöèÿ ñ÷åò÷èêà ñòðàíèö my $Current_Page_Num=0; #Äëÿ êàæäîé ïîëó÷åííîé Ãëàâû for (my $i=0; $i<=$#Manual_Parts; $i++) { $Current_Page_Num=$i; #Äëÿ êàæäîãî òåãà àíàëèçèðóåìîé Ãëàâû for (my $j=0; $j<$self->{Pages_Info}[$i]->{Num_Of_Tags}; $j++) { #Çàïîìíèòü èìÿ (òèï) òåãà â $Current_Tag_Name my $Current_Tag_Name = $self->{Pages_Info}[$i]>{Tags_Seq_Page}{$j}->{Tag_Name}; #Ïðîâåðêà: äëÿ òåãîâ çàïîìíåííîãî òèïà êîëè÷åñòâî ñòàðòîâûé è êîíöåâûõ òåãîâ â Ãëàâå íå ñîâïàäàåò if ($self->{Pages_Info}[$i]->{Tags}{$Current_Tag_Name}>{Start_Tag_Num} != $self->{Pages_Info}[$i]>{Tags}{$Current_Tag_Name}->{End_Tag_Num}) { #Ïîìåòèòü òåêóùóþ ñòðàíèöó êàê ñòàðòîâóþ â ðàìêàõ èòåðàöèîííîãî ïîèñêà ïàðíîãî çàâåðøàþùåãî òåãà $self->{Work_Container}->{Start_Page_Num}=$i;

Инициализировать счетчик: его значение соответствует общему числу стартовых тегов - тегов запомненного типа, обнаруженных в главах, которые будут проанализированы на конкретной итерации поиска парного концевого тега: $self->{Work_Container}->{Start_Tag_Num_Total} = $self>{Pages_Info}[$i]->{Tags}{$Current_Tag_Name}->{Start_Tag_Num};

Инициализировать счетчик: его значение соответствует общему числу концевых тегов запомненного типа, обнаруженных в главах, которые будут проанализированы на конкретной итерации поиска парного концевого тега: $self->{Work_Container}->{End_Tag_Num_Total} = $self>{Pages_Info}[$i]->{Tags}{$Current_Tag_Name}->{End_Tag_Num};

До тех пор пока не будет найден парный концевой тег или не будут просмотрены все главы: >{End_Tag_Num}; } #Ïðîâåðêà: êîíöåâîé òåã áûë íàéäåí if ($Current_Page_Num <= $#Manual_Parts) { #Äëÿ âñåõ Ãëàâ ó÷àñòâîâàâøèõ â èòåàðöèîííîì ïîèñêå ïàðíîãî òåãà for ($k=$self->{Work_Container}->{Start_Page_Num}; $k <= $Current_Page_Num; $k++) { #Äëÿ ñòàðòîâîé Ãëàâû èòåðàöèîííîãî ïîèñêà if ($k == $self->{Work_Container}->{Start_Page_Num}) { #Çàïîìíèòü ïîðÿäêîâûé íîìåð òåãà â Äîêóìåíòå, äëÿ êîòîðîãî íåîáõîäèìî äîáàâèòü ïàðíûé êîíöåâîé òåã â Ãëàâå


программирование push (@{$self->{Pages_Info}[$k]->{End_Tags_To_Add}}, $self->{Work_Container}->{Tag_Num_In_Doc}); $self->{Pages_Info}[$k]->{Tags}{$Current_Tag_Name}>{End_Tag_Num}++; $self->{Doc_Info}->{Tags}{$Current_Tag_Name}>{End_Tag_Num}++; } #Äëÿ çàêëþ÷èòåëüíîé Ãëàâû èòåðàöèîííîãî ïîèñêà elsif ($k == $Current_Page_Num) { #Çàïîìíèòü ïîðÿäêîâûé íîìåð òåãà â Äîêóìåíòå, äëÿ êîòîðîãî íåîáõîäèìî äîáàâèòü ïàðíûé ñòàðòîâûé òåã â Ãëàâå push (@{$self->{Pages_Info}[$k]->{Start_Tags_To_Add}}, $self->{Work_Container}->{Tag_Num_In_Doc}); $self->{Doc_Info}->{Tags}{$Current_Tag_Name}>{Start_Tag_Num}++; $self->{Pages_Info}[$k]->{Tags}{$Current_Tag_Name}>{Start_Tag_Num}++; } #Äëÿ ïðîìåæóòî÷íûõ Ãëàâ èòåðàöèîííîãî ïîèñêà else { #Çàïîìíèòü ïîðÿäêîâûé íîìåð òåãà â Äîêóìåíòå, ñòàðòîâûé è êîíöåâîé òåãè êîòîðãî áóäóò äîáàâëåíû â ïðîìåæóòî÷íóþ ñòðàíèöó push (@{$self->{Pages_Info}[$k]->{Start_Tags_To_Add}}, $self->{Work_Container}->{Tag_Num_In_Doc}); $self->{Doc_Info}->{Tags}{$Current_Tag_Name}>{Start_Tag_Num}++; $self->{Pages_Info}[$k]->{Tags}{$Current_Tag_Name}>{Start_Tag_Num}++; push (@{$self->{Pages_Info}[$k]->{End_Tags_To_Add}}, $self->{Work_Container}->{Tag_Num_In_Doc}); $self->{Doc_Info}->{Tags}{$Current_Tag_Name}>{End_Tag_Num}++; $self->{Pages_Info}[$k]->{Tags}{$Current_Tag_Name}>{End_Tag_Num}++; } } } } } } #Äëÿ âñåõ Ãëàâ for (my $i=0; $i<=$#Manual_Parts; $i++) #Âûïîëíèòü äîáàâëåíèå íåäîñòàþùèõ ñòàðòîâûõ è êîíöåâûõ òåãîâ (êîððåêòèðîâêà HTML-ñòðóêòóðû) { @{$self->{Pages_Info}[$i]->{Start_Tags_To_Add}}= sort {$b <=> $a} @{$self->{Pages_Info}[$i]->{Start_Tags_To_Add}}; @{$self->{Pages_Info}[$i]->{End_Tags_To_Add}} = sort {$b <=> $a} @{$self->{Pages_Info}[$i]->{End_Tags_To_Add}}; #Äîáàâèòü ñòàðòîâûå òåãè foreach my $value (@{$self->{Pages_Info}[$i]>{Start_Tags_To_Add}}) { $Manual_Parts[$i]=join ( , $self->{Doc_Info}>{Tags_Seq_Doc}{$value}->{Tag_Orig_Text}, $Manual_Parts[$i]); }

{

$Manual_Parts[$i] =~ s/ (<\s*?a\s{1}.*?href\s*?=\s*?»?)#$key(«?\s?.*?>)/$1ñòðîêà_httpçàïðîñà_óêàçûâàþùàÿ_ ïóòü_ñ_òî÷íîñòüþ_äî_äîêóìåíòà&page=$i$2/ is; } #Êîððåêòèðîâêà ïðî÷èõ Âíóòðåííèõ Ãèïåðññûëîê foreach $key (keys %{$self->{Pages_Info}[$i]->{Local_Links}}) { $Manual_Parts[$i] =~ s/ (<\s*?a\s{1}.*?href\s*?=\s*?»?)(#$key»?\s?.*?>)/$1ñòðîêà_httpçàïðîñà_óêàçûâàþùàÿ_ ïóòü_ñ_òî÷íîñòüþ_äî_äîêóìåíòà&page=$self>{Doc_Info}->{Local_Anchors}{$key}$2/is; }

Примечание: если ограничиться таким способом применения элементов Local_Links и Local_Links_To_Pages из структуры данных Pages_Info (см. ранее), то вместо хэшей могут быть использованы обычные списки.

Вместо заключения Возможности Perl по работе с текстом действительно велики. Это прописная истина, и в ней можно убеждаться снова и снова. {1} Хранение крупных страниц целиком в БД - это, пожалуй, не всегда является оптимальным решением. Иногда гораздо приятнее иметь структурированный HTML-архив в файлах на диске, а для удобства поиска размещать результаты его индексирования в БД.

#Äîáàâèòü êîíöåâûå òåãè foreach my $value (@{$self->{Pages_Info}[$i]>{End_Tags_To_Add}}) { $Manual_Parts[$i]=$Manual_Parts[$i]. </ .$self>{Doc_Info}->{Tags_Seq_Doc}{$value}->{Tag_Name}. > ; } } #Âåðíóòü Ãëàâû ñ îòêîððåêòèðîâàííîé HTML-ñòðóêòóðîé return @Manual_Parts; }

Ранее при постановке задачи было замечено, что корректность фрагментов HTML-кода, получаемых при разбиении документа, определяется не только его структурой но и корректными внутренними ссылками. Фрагмент программного кода, позволяющий реализовать это требование, приведен ниже. Êîððåêòèðîâêà Âíóòðåííèõ Ãèïåðññûëîê íà Ãëàâû foreach $key (keys %{$self->{Pages_Info}[$i]>{Local_Links_To_Pages}})

№1, октябрь 2002

41


программирование

ЭФФЕКТИВНОЕ

ИСПОЛЬЗОВАНИЕ ПАМЯТИ В PERL ПРИ РАБОТЕ С БОЛЬШИМИ СТРОКАМИ ДАНИИЛ АЛИЕВСКИЙ Обычно при программировании в Perl не приходится задумываться о расходе памяти. Этот язык содержит достаточно качественную систему сборки мусора. Кроме того, при исполнении Perl-программ как обычных CGI-сценариев, с запуском интерпретатора Perl на каждое обращение к скрипту, вся использованная память гарантированно освобождается при завершении скрипта. Но если Perl-скрипт обрабатывает действительно большие данные, — скажем, мегабайтные текстовые файлы — проблема разумного использования памяти может стать достаточно актуальной. Особенно это важно, если скрипт исполняется под управлением mod_perl или аналогичной среды. Если всецело положиться на встроенный сборщик мусора, может неожиданно оказаться, что процессы Web-сервера, исполняющие скрипты с помощью mod_perl, с каждым вызовом начинают занимать все больше памяти - вплоть до десятков мегабайт, постепенно поглощая всю свободную RAM. Я столкнулся с этой проблемой, когда реализовывал под mod_perl

42

сложный скрипт, предназначенный для обработки и парсинга произвольных HTML-страниц. Основным типом данных в скрипте были обычные текстовые строки. Поначалу я обращался со строками очень свободно, как это и принято в Perl и подобных языках, не задумываясь пользовался функцией substr; конкатенацией строк; регулярными выражениями; писал функции, возвращающие в результате строку (HTMLтекст Web-страницы), и т.п. Привело это к тому, что типичный HTTPDпроцесс с mod_perl'ом (Web-сервер Apache на Unix) тратил только на обрабатываемые данные в среднем несколько мегабайт. Это при том, что типичный размер HTML-страницы, которую следовало обработать, составлял всего 20-30 KB. А когда я попробовал "пропустить" через свою программу 10-мегабайтный HTML HTTPD-процесс "съел" 100 MB. При этом возникало впечатление утечки памяти - процессы, по мере своей "жизни", занимали все больше и больше обьема памяти. В процессе тестирования и экспериментов я выявил общие пробле-

мы, возникающие в Perl при работе с большими данными, и нашел способы их решения. После соответствующего переписывания, мой скрипт стал потреблять адекватное количество памяти, а утечка памяти прекратилась. Результаты своих исследований я предлагаю вашему вниманию. Итак, имеют место 2 основные общие проблемы.

ПРОБЛЕМА I Свободное употребление Perlсредств для работы со строками regexp'ов, substr, конкатенаций типа $a.$b или "$a$b" - приводит к порождению лишних копий строки, т.е. там, где по логике вещей алгоритму должно хватить 2 MB, будет потрачено 5 или 10 MB.

ПРОБЛЕМА II Если не предпринять специальных усилий, то после завершения Perl-функции рабочая память, израсходованная в этой функции, НЕ БУДЕТ освобождена! (Ситуация совер-


программирование шенно отличная от традиционной практики в языках без сборки мусора типа C++ или Pascal, когда все рабочие переменные, созданные внутри функции, уничтожаются при выходе из функции.) Это не так важно в обычном CGIскрипте, исполняемом внешним интерпретатором Perl. По завершении скрипта процесс будет полностью уничтожен вместе со всей своей памятью. Но в mod_perl или FastCGI, или в независимых приложениях, или серверах на Perl это очень существенно. Обратите внимание - описанная проблема НЕ ЕСТЬ истинная утечка памяти. Встроенный сборщик мусора действительно обеспечивает утилизацию ненужных переменных. Просто он делает это не совсем так, как можно было бы ожидать. А именно: занятая память будет использована повторно ПРИ СЛЕДУЮЩЕМ ВЫЗОВЕ той же самой функции, т.е. многократные повторные вызовы функции не будут приводить к постепенному исчерпанию RAM - явлению, которое традиционно называется утечкой памяти. Зато многократные вызовы приведут к другому: со временем будет занят наибольший объем памяти из всех, которые были нужны при различных вариантах вызова этой функции. В моем случае, после того как мои Perl-функции один раз обработали HTML-страницу размером 10 MB и соответствующий процесс с mod_perl "съел" 100 MB, он так и продолжал всегда занимать 100 MB, хотя все последующие обрабатываемые страницы были небольшими. Внешне такое поведение очень похоже на утечку - объем памяти, занятый процессом, никогда не уменьшается, но постепенно медленно увеличивается - по мере того как этому процессу случайно попадаются данные все большего размера. Теперь рассмотрим конкретные типовые задачи, возникающие при обработке данных в Perl. Я приведу примеры традиционного решения этих задач - неправильного в свете описанных проблем - и возможные варианты аккуратного решения, не приводящие к перерасходу памяти.

№1, октябрь 2002

1.Как завести внутри функции большую временную текстовую переменную, а перед выходом из функции освободить память из-под нее? Неправильное решение: sub a { my $text= "very large string.... (1 MB)"; ðàáîòàåì ñ $text; #ïðîñòî âûõîäèì èç ôóíêöèè, ïðåäïîëàãàÿ, #÷òî ñáîðùèê ìóñîðà àâòîìàòè÷åñêè îñâîáîäèò #ïàìÿòü èç-ïîä $text (êàê ýòî ïðîèñõîäèò #ñî ñòåêîâûìè ïåðåìåííûìè â C++ è Pascal) }

Правильное решение - добавить перед выходом вызов undef: sub a { my $text= "very large string.... (1 MB)"; ðàáîòàåì ñ $text; undef $text; }

Вызов undef освободит память, занятую переменной $text. Без такого вызова получаем общую проблему II).

2.Функция должна создать большую строку и вернуть ее в результате. Неправильное решение: sub a { my $text= "very large string.... (1 MB)"; return $text; } my $v= a(); ðàáîòàåì ñ $v;

Такой Perl-код "съест" не 1 мегабайт, действительно необходимый для сохранения переменной $v, а 2 мегабайта. Лишний мегабайт будет занят интерпертатором Perl при вычислении строкового выражения "a()" для последующего копирования этих данных в переменную $v. Мегабайт, занятый $v, можно впоследствии освободить вызовом "undef $v", но мегабайт, занятый при вычислении строкового выражения в правой части, по-моему, уже не освободить никак. Правильное решение - функция должна вернуть ссылку на созданную большую строку: sub a { my $text= "very large string.... (1 MB)"; return \$text; } my $v= a(); ðàáîòàåì ñ $$v;

undef $$v; #îñâîáîæäàåì ïàìÿòü, îòâåäåííóþ ôóíêöèåé a

Такой код "съест" только 1 мегабайт, который освободится при вызове undef. Проблема на самом деле довольно общая: никогда не следует писать выражение, результат которого - большая строка. Нельзя писать даже так: my $v= $text."\n";

если строка $text потенциально может быть большой (десятки килобайт или больше).

3.Как передать большую строку в функцию? Неправильное решение: sub a { my $text= $_[0]; #ïàðàìåòð $_[0] ñîäåðæèò ñòðîêó äëèíîé 1 MB ðàáîòàåì ñ $text; undef $text; } my $text= "very large string.... (1 MB)"; a($text);

В этом примере общей проблемы II нет, но память расходуется напрасно. Оператор присваивания $text= $_[0] расходует второй мегабайт под копию $text переменной $_[0] (который освобождается в конце вызовом "undef"). Если есть возможность, лучше работать непосредственно с $_[0] - т.е. с алиасом внешней переменной. А еще лучше - нагляднее - всегда передавать большие строки по ссылке. Предлагаемое правильное решение: sub a { my $text= $_[0]; #ïàðàìåòð $_[0] ñîäåðæèò ÑÑÛËÊÓ íà ñòðîêó ðàáîòàåì ñ $$text; } my $text= "very large string.... (1 MB)"; a(\$text);

4.Как выполнить конкатенацию нескольких строк, одна из которых может быть очень большой? Неправильное решение: my $newtext= "$a$text$b";

или my $newtext= $a.$text.$b;

43


программирование Если строка $text велика, то подобный код "съест" память, которую нельзя освободить (см. задачу 2). Правильное решение - конкатенировать по очереди: my $newtext= $a; $newtext.= $text; $newtext.= $b;

5.Как удалить/заместить небольшую подстроку в очень большой строке? Неправильное решение: my $text= "very large string.... (1 MB)"; $text= substr($text,10);

Такой код потратит лишний неосвобождаемый мегабайт при вычислении выражения substr($text,10) - см. задачу 2. Правильное решение - использовать так называемую "магию lvalue": my $text= "very large string.... (1 MB)"; substr($text,0,10)= "";

Правда, в документации написано, что Perl 5.004 в этом случае работал неэффективно. Но начиная с Perl 5.005 это работает прекрасно: лишняя память не расходуется. Эквивалентное правильное решение - использовать 4-й параметр substr: my $text= "very large string.... (1 MB)"; substr($text,0,10,"");

Но если предыдущий вариант в Perl 5.004 работает неэффективно, то такой вариант в Perl 5.004 вообще не скомпилируется.

6.Как выделить в большой строке большую подстроку? Скажем, как в мегабайтной строке выделить полумегабайтную подстроку, начиная со смещения 100,000? По описанным выше причинам следующий очевидный код неправилен: my $text= "very large string.... (1 MB)"; $text= substr($text,100000,500000);

В таком решении при вычислении "substr($text,100000,500000)" расходуются лишние полмегабайта, которые впоследствии невозможно освободить. Для этой задачи я не нашел крат-

44

кого и изящного решения. Возможный корректный подход использует следующую функцию substrlarge: sub substrlarge { # - Returns a reference to substr($_[0],$_[1],$_[2]) # and doesn't use extra memory when $len is very large # Example: # my $ps= substrlarge($text,500,1000000); # some actions with $$ps; # undef $$ps; # - it is an economical equivalent for # my $s= substr($text,500,1000000); # some actions with $s; my $offset= $_[1]; my $len= $_[2]; $len= length($_[0])-$offset unless defined $len; if ($len*2<length($_[0])) { my $k= 0; my $r= ""; for (;$k<$len;$k+=32768) { $r.= substr($_[0],$offset+$k,$k+32768<=$len?32768:$len$k); } return \$r; } else { my $r= $_[0]; substr($r,0,$offset)= ""; substr($r,$len)= "" if defined $_[2]; return \$r; } }

Если нужно выделить сравнительно небольшой фрагмент исходной строки (в данной реализации - меньше половины общей длины), то нужный фрагмент конструируется циклом, блоками по 32 KB. Потеря памяти при этом составляет порядка 32 KB столько расходует вычисление выражения "substr($_[0],...)" внутри цикла. Если же требуется получить большой фрагмент - больше половины исходной строки - то используется иной, более быстрый алгоритм. Создается полная копия исходной строки, после чего у нее обрезаются конец и начало, как описано в задаче 5. При этом временно занимается память под целую копию, но затем - при обрезании - занятый объем уменьшается. Так как требуемый объем памяти под конечный результат в данной ветке сравним с размером полной копии, то кратковременный расход памяти под полную копию представляется разумной платой за более высокую скорость. Обратите внимание: функция substrlarge работает непосредственно с аргументом $_[0], не копируя его во временную переменную - как это обычно делается в начале Perl-функций. Копирование типа "my $s= $_[0]"

привело бы к напрасному расходу памяти под лишнюю копию исходной строки (см. также задачу 3). С использованием функции substrlarge правильное решение будет таким: my $text= "very large string.... (1 MB)"; my $v= substrlarge($text,100000,500000); ðàáîòàåì ñ $$v;

7.При использовании регулярных выражений, с большой строкой нельзя генерировать переменные $1,$2 и пр. Скажем, следующий код неэффективен: my $text= "very large\012\012 string.... (1 MB)"; $text=~s/^(.*?\015?\012\015?\012)//s; my $prefix= $1; #ïðåäïîëàãàåòñÿ, ÷òî ýòîò ïðåôèêñ íåâåëèê

Хотя от этого регулярного выражения нам требуется, очевидно, только префикс строки $1, который может быть и небольшим, Perl все равно заполнит переменные $&, $` и $'. А одна из них будет большой - сравнимой с самой $text. Причем память из-под этих переменных автоматически не освободится. Здесь единственное известное мне правильное решение - избегать применения регулярных выражений к потенциально большим строкам. В данном случае можно было написать цикл поиска пары переводов строки на основе вызовов функции index. Можно также пользоваться "статическими" регулярными выражениями не использующими скобок (или использующими только (?:...) ). Такие регулярные выражения не заполняют переменных $1,$2,...,$&,$`,$' и соответственно не расходуют много памяти.

8.Нужно прочитать из файла или сокета большой текст. Типичное решение выглядит примерно так: my $text= ""; for (;åñòü ÷òî ÷èòàòü;) { my $buf= ÷èòàåì î÷åðåäíûå 32 KB; $text.= $buf; undef $buf; } ðàáîòàåì ñ $text; undef $text;

Хотя на вид этот код вполне аккуратный и следует приведенным выше


программирование рекомендациям, на самом деле он все-таки может привести к проблеме. А именно, если общий объем читаемого текста порядка 1 MB, то в процессе чтения в пике может израсходоваться не 1, а 2 мегабайта. Второй мегабайт потом обычно освобождается, но не гарантированно. Эта тонкая проблема, по-видимому, связана с механикой переотведения памяти в Perl. Оператор "$text.= $buf" время от времени увеличивает память, занятую переменной $text. В процессе такого переотведения интерпретатору Perl, вероятно, требуется двойной объем памяти: под прежнюю строку $text и под новый, увеличенный буфер для этой переменной. В этот момент процесс и занимает лишний мегабайт. Видимо, если переотведение происходит в конце цикла, второй мегабайт может и не освободиться: в соответствии в общей идеологией Perl "запасать буфера памяти на будущее повторное использование". Правильное решение описанной задачи - взять отведение памяти на себя. Например:

на NT 4.0 и в стандартном Perl из FreeBSD 4.2. Под ActivePerl 5.6 в Windows 2000 все оказалось несколько хуже: undef не освобождает память. (По крайней мере, TaskManager не показывает сокращения памяти у процесса Perl, пока длится 10-секундный sleep, следующий за вызовом undef.) Впрочем, к моменту, когда вы будете читать эту статью, возможно, этот недостаток уже будет исправлен фирмой ActiveState. В завершение хотелось бы сделать небольшое замечание. Если вас действительно интересует эффективность работы вашей программы - в плане экономии памяти, в плане быстродействия

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

àêêóðàòíî îòâåñòè ïîä $text 1 MB (1000000 áàéòîâ); for ($n=0; åñòü ÷òî ÷èòàòü; $n+=32768) { my $buf= ÷èòàåì î÷åðåäíûå 32 KB; substr($text,$n,32768)= $buf; #"ìàãèÿ lvalue" } if ($n<1000000) { substr($text,$n)=""; #î÷èùàåì íåíóæíûé "õâîñò" $text }

Если заранее неизвестно, что предстоит читать именно 1 MB, можно изредка (именно изредка!) аккуратно выполнять самостоятельное переотведение памяти. Для аккуратного отведения памяти можно предложить один из следующих приемов: $text=" "; $text x= 1000000; ëèáî $text=" "; vec($text,1000000-1,8)= 32; #êîä ïðîáåëà; ìîæíî èñïîëüçîâàòü ëþáîé äðóãîé ñèìâîë

Оба способа отводят ровно 1000000 байтов памяти, ничего не тратя зря. Второй способ ("магия lvalue" для функции vec) можно использовать также для переотведения памяти. Все вышеописанное протестировано и неплохо работает в ActivePerl 5.005

№1, октябрь 2002

©IGUS

45


программирование

«СТЕММЕР» МОРФОЛОГИЧЕСКИЙ АНАЛИЗ ДЛЯ НЕБОЛЬШИХ ПОИСКОВЫХ СИСТЕМ Сейчас уже никого не удивишь поисковой системой со встроенным русским, украинским или английским морфологическим анализатором, однако такой модуль достаточно дорог, и использование его в небольших продуктах не всегда коммерчески оправданно. Поисковые и не только поисковые системы Интернет столь популярны сегодня, что люди проводят часы, обсуждая достоинства и недостатки той или иной, алгоритмы и программы для поиска полнотекстовой информации на тех или иных носителях. И все это время не утихают горячие споры «профи» поиска и «ленивых пользователей». Первые - сторонники чисто «механических» машин, поисковых систем, которые вычисляют строгие логические запросы и поддерживают усечение слова справа «звёздочкой»; они убеждены, что лучше всяких алгоритмов сформулируют, что же им нужно найти. Другие - наоборот, стараются отдать на откуп алгоритмам поисковика все магические преобразования исходного запроса и не задумываться о том, что же там происходит внутри. Обе точки зрения имеют право на существование, и я даже знаю нескольких таких профи, которые способны действительно грамотно сформулировать логический запрос на поиск нужной информации, корректно расставив скобки и применив соответствующие усечения. Однако подавляющее большинство пользователей, к коим я отношу и себя, все-таки скорее «лентяи».

46

Один достаточно иллюстративный диалог произошел на выставке SofTool в 1995 году. Компания «Агама», где я в тот момент работал, представляла одноименную систему поиска информации, размещенной на локальных дисках компьютера. Надо сказать, эта поисковая система уже тогда, несмотря на убогость DOS, под управлением коего она работала, уже делала полноценный словарный морфологический анализ обрабатываемых текстов и поисковых запросов, то есть умела искать с учетом всех форм русских слов. И вот одна дама, протестировав систему и побеседовав о морфологической обработке текстов с господином Пархоменко, идеологом и руководителем проекта, воскликнула на весь зал: «Да ведь это никому не нужно! Я сама прекрасно все найду, дайте мне только оператор усечения!». На беду, оператор усечения, как и весь традиционный набор логических операторов, в языке запросов присутствовал. После получаса стучания по клавишам дама со словами: «Вы меня не убедили!», - покинула стенд. Действительно, лицензировать систему морфологического анализа за несколько тысяч долларов для использования ее в разработке продук-

та под конкретного заказчика, когда бюджет проекта не сильно превышает эту сумму - не самая лучшая идея. Что же делать в такой ситуации? Есть два решения. Первое - не использовать лингвистических алгоритмов вообще, если это возможно. Второе остановиться на стемминге, то есть на формальном выделении основы - стабильной, графически неизменной при склонении или спряжении, части слова. Программы, выполняющие стемминг, или стеммеры, также существуют примерно столько же, сколько и поисковые системы. И, надо отметить, в случае английского языка, (достаточно простого и не склонного к излишней флективности, то есть изменчивости слов), стеммеры успешно справляются с поставленной перед ними задачей. Так, классический «портеровский» алгоритм, хоть он и грешит сведением упоминавшейся аббревиатуры DOS к формальной основе - глаголу do, тем не менее дает вполне корректное значение отношения «сигнал/шум» в случае поиска информации. Хуже обстоит дело со славянскими языками. Выделить правила, по которым можно отсечь часть слова справа, да так, чтобы не породить


программирование сильного шума, очень сложно, и построение такого набора правил сравнимо по трудоемкости с построением полноценного словарного морфологического анализатора, чем обычно и заканчиваются доведенные до логического завершения разработки в этой области. Приведем простой пример - слово кровать. До сих пор все программы стемминга для русского языка успешно и совершенно логично признавали это слово изменяющимся по модели глагола, выделяя или формальную основу «кр» или в лучше случае, «кров». Уважаемый читатель, мне кажется, по достоинству оценит такой глагол - «я крую/кроваю, он кровает/круёт». Однако за истекший год в этом направлении были сделаны серьезные шаги. Мартин Портер (Martin Porter) сделал доступными свои наработки в области стемминга, наборы правил и инструменты для работы с ними, для открытого сетевого сообщества, опубликовав проект на S o u r c e F o r g e ( h t t p : / / snowball.sourceforge.net/).Проект представляет собой специализированный язык обработки строк, предназначенный для изготовления алгоритмов стемминга в информационном поиске. Довольно быстро проект стал расширяться, и сейчас доступны в исходных текстах стеммеры для английского, французского, испанского, португальского, итальянского, немецкого, датского, шведского и норвежского языков. Русский поначалу остался в стороне, однако недавно появилась поддержка и для него (http:// snowball.sourceforge.net/russian/ stemmer.html). Тем не менее, по ряду причин, часть из которых обсуждалась выше, стеммер Snowball все равно далек от идеала. И основная причина заключается в том, что он базируется на обобщенных правилах, составленных людьми, в лучшем случае - лингвистами. А как известно, любое общее правило имеет исключения, приводящие в данном случае к неправильному выделению основы. Еще одно нарекание - это «убежденность алгоритма в собственной непогрешимости»: алгоритм всегда дает один-единственный вариант формальной основы слова, игнорируя все остальные по

№1, октябрь 2002

внутренним соображениям. Именно в силу указанных недостатков и было принято решение о разработке своего алгоритма и правил стемминга. Отмечу сразу, что мыслей разрабатывать вручную правила усечения русских слов не было даже изначально, но было большое желание изготовить алгоритм, который учитывал бы и сочетаемость букв в слове на границе возможного усечения, и частотность такой модели словоизменения. Поэтому было принято решение строить правила стемминга автоматически, обрабатывая большие массивы русских текстов. Задача сильно облегчалась наличием русского морфологического анализатора, разработанного еще в 1994 году во время работы над проектом «Пропись 4.0». Анализатор этот работает и поныне в поисковых системах Апорт! (www.aport.ru),Рамблер (www.rambler.ru), <META> (www.metaukraine.com), хотя и пережил несколько преобразований, в результате чего словарь уже придирчиво выверен, а код позволяет обрабатывать до 20 30 тысяч слов в секунду. Подробнее с этим анализатором и условиями его распространения можно ознакомиться на странице автора (linguist.nm.ru). Для представления автоматических правил усечения слов была выбрана модель хранения возможного окончания с двумя предшествующими буквами неизменяемой части слова. Так, например, словоформа словарями порождает правило, разрешающее отщепление окончания -ями при условии, что ему предшествует последовательность -ар-. Аналогично, встретив словоформу морями, мы породим правило о возможном отщеплении того же окончания (-ями) при ус-

ловии, что оно встретилось после фрагмента -ор-. Далее была изготовлена программа обработки массивов полнотекстовой информации, которая, разбив текст на слова, выполняла обработку очередной потенциальной словоформы точным морфологическим анализатором; при этом неизвестные словарному анализатору строки игнорировались, что является допустимой погрешностью, поскольку, имея базу более 150,000 основ и распознавая более четырех миллионов различных форм русских слов, анализатор игнорирует менее одного процента встретившихся строк, которые по большей части оказываются либо орфографическими ошибками, либо аббревиатурами, либо экзотическими названиями или именами собственными. Для опознанных же словоформ выделялась их точная основа, то есть часть слова, остающаяся неизменной при склонении или спряжении; выделенное таким способом окончание вкупе с последними двумя символами формальной основы поступало в накопитель, который либо регистрировал новое правило, либо увеличивал вес уже существующего правила отщепления окончания. По завершении работы сканера текстов получившийся массив данных был отранжирован в соответствии с убыванием вероятности встретить каждую из присутствующих моделей словоизменения, после чего модели, вероятность реализации которых составляла менее 10-4, были отброшены как редкие и потенциально опасные, т. е. способные породить избыточный шум. Результат - набор потенциальных окончаний с условиями на предше-

47


программирование ствующие символы - был инвертирован для удобства сканирования словоформ «справа налево» и представлен в виде таблицы переходов конечного автомата. Подробнее устройство таких таблиц описано в статье о словарном морфологическом анализаторе, доступной по адресу http:// linguist.nm.ru/ling/rus/help.htm. Мы же лишь покажем на рисунке фрагмент такой таблицы, построенный для небольшого набора окончаний и условий на предшествующие символы основы. В качестве примера выберем уже упоминавшиеся формально построенные правила: (-ар-)-ями, (-ор-)ями, добавив к ним для наглядности еще два - (-ск-)-и и (-сн-)-а, построенные, например, по словоформам диски и весна. Далее был разработан переносимый программный код на языке C, обеспечивающий сканирование подаваемых на вход форм слов на полученных таблицах переходов. Особо следует отметить, что код этот не берет на себя ответственность за выбор конкретного способа усечения, но лишь строит все допустимые варианты выделения формальной основы. Так, для словоформы начинающихся анализатор выделит формальные основы длиной 6, 8 и 10 символов, что соответствует глаголу начина-ть, причастию начинающ-ий и возвратной форме - начанающий-ся, то есть выделит псевдоморфемы, участвующие в образовании этого слова. Программа-клиент, например поисковая машина, вправе либо самостоятельно принять решение о выборе того или иного варианта усечения, либо запомнить все построенные варианты, что обеспечит максимальную полноту поиска. С другой стороны, для многострадального слова кровать обученный таким образом алгоритм выделит лишь одну формальную основу - шестибуквенную последовательность кроват-ь, что полностью соответствует истине. В ряде ситуаций анализатор принимает решение о невозможности какого-либо усечения по данной последовательности, как, например, в случае слова компьютер, что также полностью соответствует действительности. По ходу тестирования и отладки построенной технологии было введе-

48

но дополнительное правило, ограничивающее свободу алгоритма. Суть правила состоит в том, что формальная основа слова должна содержать хотя бы одну гласную, иначе возможно построение весьма некорректных основ, как, например, усечение слова спам до основы сп, что, как известно, является распространенной аббревиатурой словосочетания «совместное предприятие». Интересно, что большинство стеммеров, в том числе и используемый в поисковой системе Яndex (www.yandex.ru) для обработки неизвестных слов, грешат в подобных ситуациях, в чем несложно убедиться, дав соответствующий запрос. Для обеспечения должной производительности анализатор не строит строк, а лишь возвращает набор возможных длин формальных основ, которые можно выделить из строки. Инициализация модуля также не требуется, так как все таблицы переходов представлены статическими данными. Особое внимание пришлось уделить поддержке различных кодовых страниц и возможной капитализации - начертанию слов в верхнем регистре. Действительно, при всей привлекательности кодировки 1251 Windows Cyrillic и удобстве работы с ней, она не является ни единственной, ни даже самой популярной. Преобразовывать же строку из иных кодировок в желаемую перед анализом весьма и весьма дорого по меркам «скорострельных» алгоритмов. Для обеспечения совместимости анализатора с текстами в различных кодировках предусмотрена возможность его настройки передачей дополнительного параметра - таблицы преобразования кодов символов из произвольной кодировки в нижний регистр кодировки 1251

размерностью 256 байт. Если этот параметр не задан, то по умолчанию используется встроенная таблица преобразования символов верхнего регистра кодовой страницы Windows Cyrillic в символы нижнего регистра. Хочется отметить, что весной 2002 года в лаборатории общей и компьютерной лексикографии кафедры русского языка Московского Государственного университета было проведено сравнительное тестирование трех программ русского стемминга: Snowball, описываемого алгоритма и стеммера, разработанного там же под руководством докторов филологических наук А. А. Поликарпова и О. В. Кукушкиной. По результатам тестирования описываемый стеммер показал наиболее корректные в лингвистическом отношении результаты при максимальной полноте и минимальном шуме, то есть количестве неверно выделенных основ. Позже описанным способом был разработан также словарь стемминга украинского языка, который вместе с русским успешно применяется для анализа неизвестных слов в украинской поисковой системе <META> (www.meta-ukraine.com), а также изготовлена программа, позволяющая автоматически строить таблицы стемминга для других языков из словарей ISpell, доступных в сети Интернет. Однако следует оговориться, что в последнем случае качество получаемого словаря напрямую зависит от качества используемых материалов, которое чаще всего весьма низко. Представленный продукт доступен в исходных текстах и может использоваться в свободной форме с условием ссылки на источник. Архив можно загрузить по адресу http:// linguist.nm.ru/stemka/stemka.tar.gz.

Зовут меня Андрей Коваленко, я работаю в настоящий момент в компании Рамблер, занимаюсь развитием поисковой машины, до этого работал в МедиаЛингве, еще раньше в Агаме, где занимался разработкой поисковой системы Апорт!. За несколько лет занятий прикладной лингвистикой у меня накопилось некоторое количество наработок, которыми я бы с удовольствием поделился с другими разработчиками. Кое-что доступно бесплатно, кое-что - только в виде демонстрационных версий.


ОПРЕДЕЛЕНИЕ Графический лист из серии «Смыслограммы»

©1990 Художник Игорь УСКОВ www.igus.nm.ru


JAVA: МАГИЯ ОТРАЖЕНИЙ

ЧАСТЬ I. ОСНОВЫ

ДАНИИЛ АЛИЕВСКИЙ


программирование Один из самых удивительных и ярких механизмов языка Java — технология «отражения» (Java Reflection). К сожалению, в популярных учебниках нелегко найти подробную информацию об этой интереснейшей области. А тем более — о подводных камнях и неожиданных возможностях, возникающих при программировании с использованием отражений. Между тем, именно отражения позволяют Java с неподражаемым изяществом справляться с задачами, традиционно непростыми в других языках — такими, как создание оболочки для компиляции Java-проектов (наподобие Borland JavaBuilder или NetBeans), визуальное проектирование графических компонентов (JavaBeans), сериализация объектов и распределенные вычисления (RMI), и многие другие. Мне бы хотелось предложить Вам небольшую экскурсию по миру отражений Java. По ходу дела я буду показывать, как использовать мощь этой технологии для изящного решения различных практических задач. Мы начнем с простых вещей — простых для Java, но не столь тривиальных в других языках. Пример такой «простой вещи» — динамическое расширение вашей программы новыми классами от сторонних разработчиков, не требущее переписывания и даже перекомпиляции вашей системы. По мере знакомства с отражениями, задачи будут усложняться. В качестве завершающего примера, полностью использующего возможности отражений, я расскажу, как реализовать высокоэффективный интерпертатор выражения (формулы), написанного на языке Java — эквивалент оператора eval() в скриптовых языках типа Perl или JavaScript. Все написанное ниже относится к последней (на момент написания статьи) версии Java: Sun Java SDK 1.4.

Где искать мир отражений? Отражения в Java — это два класса Class и ClassLoader, расположенных в пакете java.lang, и специальный пакет java.lang.reflect, содержащий (в версии Java SDK 1.4) 12 вспомогательных классов: Array, Member, Constructor, Field, Method, Modifier, InvocationHandler, Proxy, ReflectAccess, ReflectPermission, InvocationTargetException, UndeclaredThrowableExceptioCfn. Проще всего осваивать технику отражений, начиная с класса Class. Более сложные вещи потребуют применения классов из пакета java.lang.reflect, прежде всего классов, описывающих: Constructor, Field и Method. «Высший пилотаж» работы с отражениями — это, пожалуй, создание грамотных наследников ClassLoader, позволяющих реализовать собственную систему загрузки классов. В этой статье я покажу, как грамотно использовать эти классы и их методы. В любом случае, статья не заменяет справочную документацию фирмы Sun. Самую полную и свежую фирменную документацию можно найти в Internet по адресу: http://java.sun.com/

Класс по имени Class В мире отражений все начинается с класса «java.lang.Class». Для каждого Java-класса или интерфейса, присутствующего в системе, существует экземпляр Class, содержащий подробную низкоуровневую информацию об этом классе. Например, с помощью этого экземпляра мож-

№1, октябрь 2002

но узнать списки членов класса — полей, методов, конструкторов, а также обратиться к этим членам. Кроме того, класс Class содержит ряд статических методов, обеспечивающих взаимодействие с внутренними механизмами Java, отвечающими за загрузку и управление классами. Как получить экземпляр Class, соответствующий данному классу (или интерфейсу) — например, классу java.io.File? Для этого есть два основных способа. A. Просто добавляем к имени класса суффикс «.class», например: Class clazz= byte.class

(«clazz» — сознательно искаженное от «class»: компилятор не позволяет использовать в качестве идентификатора зарезервированное слово «class».) B. Если мы располагаем экземпляром некоторого класса, может быть даже неизвестного в данной точке программы, можно вызвать метод getClass(), присутствующий в каждом Java-объекте (унаследованный от класса Object): void myFunction(Object o) { Class clazz= o.getClass(); ÷òî-òî äåëàåì ñ îáüåêòîì clazz; } ... java.io.File f= new java.io.File("/tmp/1.txt"); myFunction(f); ...

Здесь есть любопытный нюанс. Вообще-то, примитивные типы Java — boolean, char, byte, short, int, long, float, double — обычно не считаются полноценными классами. Они не унаследованы от Object, для них не работает наследование и т.д. Тем не менее, с ними тоже ассоциированы экземпляры Class, которые можно получить способом A, например: Class clazz= java.io.File.class;

Существует даже специальный объект void.class — он используется в довольно экзотических ситуациях при вызове методов через отражения. Экземпляры типа Class есть также у любого массива, например: Class clazz= byte[].class èëè byte[] v= new byte[34]; Class clazz= v.getClass();

Что же можно сделать, располагая переменной типа Class для некоторого класса? Прежде всего, можно получить полное имя класса (скажем, для отладочной печати) методом getName(). Например: String.class.getName() возвращает «java.lang.String». Интересно посмотреть на имена классов для примитивных типов и для массивов: float.class.getName() âîçâðàùàåò «float» float[].class.getName() âîçâðàùàåò «[F» float[][].class.getName() âîçâðàùàåò «[[F» String[].class.getName() âîçâðàùàåò [Ljava.lang.String;»

Полностью алгоритм построения такого имени описан в документации на Java.

51


программирование Можно проверить, не является ли класс интерфейсом, массивом или примитивным типом: методы isInterface(), isArray() и isPrimitive(). Если класс — массив, можно получить тип его элементов (т.е. соответствующий объект Class): метод getComponentType(). Для не-массивов этот метод вернет null. Даже такие простейшие механизмы позволяют сделать нечто полезное. Вот пример — функция для подсчета объема памяти, занятого массивом примитивных типов: public static int sizeOfArray(Object v) { if (v==null) return 0; Class componentClass= v.getClass().getComponentType(); if (componentClass==null) throw new IllegalArgumentException(«Not an array»); int s= componentClass==byte.class? 2: componentClass==short.class? 4: componentClass==char.class? 4: componentClass==int.class? 4: componentClass==float.class? 8: componentClass==long.class? 8: componentClass==double.class? 16: -1; // íå îáðàáàòûâàåì òèï boolean: äëÿ íåãî ðàçìåð ýëåìåíòà // íå ñïåöèôèöèðîâàí äîêóìåíòàöèåé ïî ÿçûêó Java if (s==-1) throw new IllegalArgumentException(«Unknown component type»); return java.lang.reflect.Array.getLength(v)*s; // ìåòîä getLength() êëàññà java.lang.reflect.Array ïîëó÷àåò // íà âõîäå ìàññèâ ïðîèçâîëüíîãî òèïà è âîçâðàùàåò åãî äëèíó }

java.lang.reflect.Array — «странный» способ работать с массивами Прежде чем переходить к более мощным (и ценным) возможностям класса Class, давайте обратим внимание на java.lang.reflect.Array. Этот очень простой класс из пакета java.lang.reflect представляет собой библиотеку статических методов, позволяющих выполнять все основные примитивные действия с массивом неизвестного (на этапе компиляции) типа, представленного общим типом данных Object. Пример мы видели в приведенной выше функции sizeOfArray() — метод getLength(). Этот метод объявлен в исходном коде фирмы Sun следующим образом: public static native int getLength(Object array) throws IllegalArgumentException;

Любой массив в Java является объектом, т.е. наследником Object, поэтому его можно передать в getLength() в качестве параметра. На самом деле, только массивы и можно передавать в этот метод — как и в большинство других методов класса Array. Для объектов других типов методы класса Array возбуждают исключение IllegalArgumentException. Большинство остальных методов класса Array предназначены для чтения и записи элементов в массив: public static Object get(Object array, int index)

возвращает элемент номер index; если массив состоит из элементов примитивного типа (byte, char и т.п.), возвращается объект-оболочка (Byte, Char, ...); public static boolean getBoolean(Object array, int index), public static char getChar(Object array, int index)

52

è àíàëîãè÷íûå ìåòîäû äëÿ òèïîâ byte, short, int, long, float, double

специальные версии get на случай, когда массив состоит из элементов соответствующего примитивного типа; public static void set(Object array, int index, Object value), public static void setBoolean(Object array, int index, boolean z), public static void setChar(Object array, int index, char c) è ò.ä.

обратные функции для записи элементов в массив. Наконец, можно создать массив c помощью одного из двух методов: public static Object newInstance(Class componentType, int length), public static Object newInstance(Class componentType, int[] dimensions)

Первый метод удобен для одномерных массивов, второй — позволяет создавать сразу многомерные массивы. В качестве componentType передается класс элементов — например, один из классов примитивных типов boolean.class, char.class и т.д. Все подробности — в документации фирмы Sun. На первый взгляд у неискушенного Java-программиста все это вызывает некоторое недоумение. Зачем столь замысловатым способом манипулировать с массивом, когда можно просто воспользоваться встроенными языковыми средствами? Зачем писать: v.getByte(n)

для массива v типа byte[], когда можно просто написать v[n]? Ответ — класс Array позволяет писать универсальные функции, принимающие на вход массив произвольного, неизвестного заранее типа. Пример такой функции мы уже видели — sizeOfArray() из предыдущего пункта. Если бы не класс Array, нам бы либо пришлось написать 7 версий этой функции для каждого варианта массивов примитивных типов, либо заменить вызов java.lang.reflect.Array.getLength(v) чем-то вроде: v instanceof byte[]? ((byte[])v).length: v instanceof short[]? ((short[])v).length: v instanceof char[]? ((char[])v).length: v instanceof int[]? ((int[])v).length: v instanceof long[]? ((long[])v).length: v instanceof float[]? ((float[])v).length: v instanceof double[]? ((double[])v).length: -1

Приведем более практичный пример использования класса Array. Достаточно часто возникает задача преобразовать произвольный объект в текстовую строку — например, в целях отладки. Предназначенный для этого метод toString() обычно выдает достаточно внятную информацию о содержимом объекта. Но, к сожалению, для массивов этот метод ведет себя весьма тупо — просто возвращает внутренний адрес массива. Я написал простую функцию toS(Object v, String separator), преобразующую произвольный массив в стро-


программирование ку. Все элементы массива преобразуются (стандартным образом) в строки и конкатенируются через разделитель separator, заданный в качестве параметра функции. Вот текст этой функции: public static String toS(Object v, String separator) { if (v==null) return «»; if (v.getClass().isArray()) { int len; if ((len=java.lang.reflect.Array.getLength(v))==0) return «»; StringBuffer result= new StringBuffer(); for (int k=0; k<len; k++) { if (k>0) result.append(separator); result.append(String.valueOf( java.lang.reflect.Array.get(v,k))); } return result.toString(); } return String.valueOf(v); }

Если аргумент v не является массивом, действие toS не отличается от стандартного метода v.toString(). Если v==null, возвращается пустая строка (обычно это удобнее стандартной реакции — возврата строки «null»). Без класса Array пришлось бы написать 9 вариантов такой функции — для 8 примитивных типов и для массива объектов произвольного типа Object[]. Здесь нужно сделать одно важное замечание. Хотя класс Array действительно позволяет существенно экономить текст программы и не писать разные варианты метода для массивов разных типов, следует иметь в виду — получаемый код сравнительно неэффективен. Скажем, цикл суммирования всех элементов числового массива через вызов java.lang.reflect.Array.getDouble() будет работать на порядок дольше банального: double s= 0.0; for (int k=0; k<v.length; k++) s+= v[k];

В случае функции toS() разница была бы непринципиальной, так как преобразование числа в строку — сравнительно медленная операция.

Class.getResourceAsStream()- ресурсы Один из самых очевидных примеров использования класса Class — загрузка ресурсов. Ресурс в Java — это файл с данными, прилагаемый к Вашей программе и обычно размещаемый «рядом» с class-файлами. В графических приложениях и апплетах это чаще всего изображения (jpg- или gif-файлы), но с таким же успехом это может быть и файл специального формата — например, содержащий справочные таблицы или какие-либо ваши объекты. Наиболее общий способ прочитать файл-ресурс произвольного формата, расположенный «рядом» с некоторым классом — обратиться к методу getResourceAsStream() объекта Class, соответствующего данному классу. В качестве параметра этому методу нужно передать путь к файлу ресурса относительно каталога, в котором размещен ваш класс. В качестве результата getResourceAsStream() вернет объект InputStream, с помощью которого можно прочитать файл ресурса стандартными средствами ввода/вывода Java. Для наиболее популярных типов ресурсов, таких как

№1, октябрь 2002

изображения, обычно существуют более удобные способы прочитать ресурс — например, метод getImage() класса java.applet.Applet. Но для текстовых файлов и файлов нестандартного формата getResourceAsStream(), как правило, — самое разумное решение. Вот пример законченного класса, использующего эту технику: import java.io.*; public class MyClassWithResource { public static final String myTextResourceName= «mydata.txt»; public static final String myTextResource; static { String s= «»; try { InputStream stream= MyClassWithResource.class .getResourceAsStream(myTextResourceName); if (stream==null) throw new FileNotFoundException(myTextResourceName+» not found»); StringBuffer sb= new StringBuffer(stream.available()); InputStreamReader reader= new InputStreamReader(stream); char[] buf= new char[32768]; int len; while ((len=reader.read(buf,0,buf.length))>=0) { sb.append(buf,0,len); } s= sb.toString(); } catch (IOException e) { e.printStackTrace(); } myTextResource= s; } public static void main(String[] args) { System.out.println(«Loaded resource:»); System.out.println(myTextResource); } }

В этом примере файл «mydata.txt» должен быть расположен в том же каталоге, что и class-файл «MyClassWithResource.class». Файл ресурса необязательно размещать в том же каталоге, что и class-файл. Если он расположен в одном из подкаталогов этого каталога, в качестве имени ресурса нужно передать относительный путь, разделяя имена подкаталогов символом «/» (как это принято в Internet и Unix). Можно также указать в качестве имени ресурса «абсолютный» путь, начинающийся с символа «/». Тогда Java будет искать ресурс во всех каталогах, перечисленных в путях поиска классов CLASSPATH — т.е. по тем же правилам, по которым отыскиваются class-файлы программы. Может возникнуть вопрос — зачем нужен специальный метод класса Class, когда можно прочитать файл ресурса обычными средствами файлового ввода/вывода? Основная причина — использование метода getResourceAsStream() является гораздо более общим решением, работающим в большем числе ситуаций. Например, по традиции, законченные наборы классов — Java-приложения или библиотеки — принято упаковывать в архивы JAR и устанавливать на компьютер именно в таком виде. Классы Java прекрасно загружаются непосредственно из архива JAR, без предварительной распаковки. То же самое относится и к ресурсам, загружаемым методом getResourceAsStream() — или более специальными методами типа java.applet.Applet.getImage(). В то же время, обычные средства файлового ввода/вывода для чтения ресурса из JAR уже непригодны — нужно использовать специальные классы для анализа и чтения JAR-файлов.

53


программирование Аналогичная ситуация — апплеты, когда файлы ресурсов и class-файлы находятся на сервере. Метод getResourceAsStream() в этом случае обеспечит чтение ресурса с сервера через Internet. Тем не менее, в некоторых случаях для чтения ресурса все-таки может быть целесообразным использование традиционных средств файлового ввода/вывода — конечно, если вы не используете JAR и пишете сервлет или приложение, а не апплет. Например, для достижения максимальной производительности может понадобиться отобразить ресурс в память средствами отображения файлов из пакета java.nio, появившегося в Java начиная с версии SDK 1.4. Или, может быть, вы захотите просканировать каталог с ресурсами и загрузить все файлы с определенным расширением — для сканирования каталога в классе Class нет соответствующих средств. Если ресурс, как это принято, расположен рядом с class-файлами — или, скажем, если вы хотите поработать с самими class-файлами вашего приложения — то придется решить следующую задачу: определить дисковый каталог, в котором находится ваш class-файл. Будьте внимательны: это отнюдь не простая задача. Ниже приведен текст функции, возвращающей полный путь к class-файлу. public static java.io.File getClassFile(Class clazz) { // The file will exist only if it is usual class-file, // not a part of JAR or Web resource String s= clazz.getName(); s= s.substring(s.lastIndexOf(«.»)+1); s= clazz.getResource(s+».class»).getFile(); try { s= java.net.URLDecoder.decode(s,»UTF-8"); } catch(java.io.UnsupportedEncodingException e) { } return new java.io.File(s); }

Прежде всего мы получаем имя класса без имени пакета, добавляем к нему расширение «.class» и передаем методу getResource() объекта Class. Этот метод возвращает экземпляр класса java.net.URL, представляющий файл в виде универсального пути к ресурсу (URL, Universal Resource Locator). В нашем случае (когда классы расположены в обычных файлах в локальной файловой системе) URL будет выглядеть примерно так: file://ïóòü_ê_äèñêîâîìó_êàòàëîãó/èìÿ_êëàññà.class

Метод getFile() объекта URL «отрежет» префикс «file:/ /», оставив все остальное без изменений. А вот дальше начинаются сложности. Оставшийся путь к файлу записан в стандарте URL, который может отличаться от формата имен файлов, принятого в текущей файловой системе. Первое отличие: в URL подкаталоги всегда разделяются прямым слэшем /, в то время как в операционных системах, отличных от Unix, могут использоваться другие символы (например, обратный слэш \ в случае Windows). Это различие несущественно для Java — классы пакета java.io прекрасно будут работать и с именем файла, записанным через прямые слэши. Но есть и второе отличие. Если путь к файлу содержит

54

пробелы, русские буквы или другие символы, недопустимые в стандарте URL, то они будут закодированы комбинациями вида %XX, где XX — ASCII-код символа. Такое имя файла непригодно для обработки средствами ввода/вывода Java. Для восстановления «нормального» имени файла нужно вызвать метод java.net.URLDecoder.decode(), обязательно указав при этом кодировку символов (encoding). Опытным путем я установил, что для кодирования нелатинских букв в имени файла Java использует кодировку UTF-8 — по крайней мере, на Windows-платформе. К сожалению, я не нашел в документации прямых указаний, что это всегда будет так на всех платформах. Поэтому я бы порекомендовал, по возможности, все же размещать свои классы в каталогах, путь к которым состоит только из латинских символов — тогда приведенная выше функция будет достаточно надежна.

Class.forName() и Class.newInstance() — динамическая загрузка классов Мы подошли к рассмотрению по-настоящему важных и интересных технологий мира отражений. Речь пойдет о фундаметальном и чрезвычайно мощном механизме Java: динамической загрузке произвольного класса по заданному имени. Эта возможность встроена непосредственно в Java и реализуется классом Class. В других языках типа С++ или Delphi аналогичных целей можно достигнуть, используя специальные средства конкретной операционной системы (типа загрузки dll в Windows функцией loadLibrary()), но в Java это сделано по-настоящему удобно. Итак, Вашему вниманию предлагается статический метод Class.forName(). Вот его формальное объявление: public static Class forName(String className) throws ClassNotFoundException

Метод отыскивает в системе (среди путей поиска классов CLASSPATH) класс с заданным именем className и возвращает соответствующий экземпляр класса Class. Имя className должно быть полным, т.е. включать имя пакета. Например: Class clazz= Class.forName(«java.lang.String»);

Если такой класс отсутствует, возбуждается исключение ClassNotFoundException. После получения переменной типа Class, следующее наиболее типичное действие — создание экземпляра только что загруженного класса. Для этого служит метод Class.newInstance(). Его объявление: public Object newInstance() throws InstantiationException, IllegalAccessException

В нашем примере вызов newInstance() мог бы выглядеть так: Object object= clazz.newInstance();

Чтобы этот метод мог выполниться, у загруженного клас-


программирование са должен существовать пустой конструктор (без аргументов), либо — что по существу то же самое — не должно быть описано вообще никаких конструкторов. В противном случае будет возбуждено исключение InstantiationException. Конечно, этого еще мало, чтобы работать с полученным объектом. Если вы не знаете, что умеет делать класс — какие у него есть методы, что они ожидают получить на входе и для чего они предназначены — вы не сможете извлечь из него ничего полезного. Для формального определения, что «умеет» класс, в Java существует стандартный механизм — интерфейсы. Остается просто применить этот механизм. Например, предположим, ваша программа должна в некоторые моменты выполнять перевод с одного языка на другой. Формально это можно описать интерфейсом: public interface LanguageTranslator { public String translate(String source, String sourceLanguage, String targetLanguage); // ïåðåâîäèò òåêñò source ñ ÿçûêà sourceLanguage // íà ÿçûê targetLanguage }

Пусть Ваша программа сама по себе не умеет выполнять перевод, но ее можно расширить классами сторонних разработчиков, которые эту задачу решать умеют. Все эти классы реализуют интерфейс LanguageTranslator и имеют конструктор без параметров (либо лишены конструктора). В настройках Вашей программы пользователь указывает имя такого класса, независимо инсталлированного в систему (или выбирает из списка классов), после чего для выполнения перевода ваша программа использует следующие операторы: String source= «òåêñò, òðåáóþùèé ïåðåâîäà»; String sourceLanguage= «Russian»; String targetLanguage= «English»; Class clazz= Class.forName(«ïîëíîå_èìÿ_êëàññà_ïåðåâîä÷èêà»); Object object= clazz.newInstance(); if (!(object instanceof LanguageTranslator)) { throw new Exception(«...»); // ñîîáùàåì îá îøèáêå: óêàçàííûé êëàññ // íå ðåàëèçóåò òðåáóåìûé èíòåðôåéñ, // ò.å. íå ÿâëÿåòñÿ ïåðåâîä÷èêîì } String result= ((LanguageTranslator)object) .translate(source,sourceLanguage,targetLanguage);

Описанная техника может оказаться очень полезной практически в любой достаточно большой и серьезной системе, рассчитанной на разработку многими участниками. Многие компоненты таких систем являются достаточно изолированными, и их набор может быть совершенно неизвестен на этапе компиляции основной программы — известны лишь интерфейсы, который они обязуются реализовывать. Например, так обычно строятся системы plugin’ов — модулей, добавляемых к уже работающей системе. В подобных случаях механизм отражений — методы Class.forName() и Class.newInstance() — становится единственным грамотным решением.

Constructor, Field, Method — работа с классами через отражения На самом деле технология отражений позволяет сделать гораздо больше, чем просто загрузить по имени некоторый

№1, октябрь 2002

(заранее неизвестный) класс и создать его экземпляр. Можно получить полный список всех конструкторов, полей и методов класса и обратиться к любому из них, передав (в случае конструктора или метода) список всех параметров. Для этого служат следующие методы класса Class: public public public public public public

Constructor[] Field[] Method[] Constructor[] Field[] Method[]

getConstructors(), getFields(), getMethods(), getDeclaredConstructors(), getDeclaredFields(), getDeclaredMethods()

Перечисленные методы возвращают массивы объектов типа Constructor, Field и Method. Эти классы содержатся в пакете java.lang.reflect и обеспечивают исчерпывающий доступ к полям, конструкторам и методам. Сразу бросается в глаза наличие двух версий методов: getXXX и getDeclaredXXX (где XXX — «Constructors», «Fields» или «Methods»). Поначалу это может даже несколько сбить с толку — какой версией следует пользоваться? getDeclaredXXX возвращает список членов класса (конструкторов, полей или методов), объявленных при описании класса, но не унаследованных от классов-предков. При этом в список включаются все члены, независимо от их уровня защиты — т.е. public, protected, private и дружественные члены. getXXX возвращает полный список членов, объявленных в самом классе либо унаследованных от одного из предков. Но в этот список уже попадают только public-члены. При желании, конечно, можно получить и максимально полную информацию — список всех членов, объявленных в самом классе либо в любом из его предков. Для этого достаточно организовать цикл по цепочке классов-предков, пользуясь специальным методом Class.getSuperclass(). Кроме получения полного списка, можно также отыскать в классе конкретный член. Для этого служат методы: public Constructor getConstructor(Class[] parameterTypes), public Field getField(String name), public Method getMethod(String name, Class[] parameterTypes), public Constructor getDeclaredConstructor(Class[] parameterTypes), public Field getDeclaredField(String name), public Method getDeclaredMethod(String name, Class[] parameterTypes)

(Собственно, чаще используются как раз эти методы, а не описанные выше методы получения списков.) Аргумент name в этих методах должен содержать имя требуемого члена. Аргумент parameterTypes связан с возможностью Java перегружать конструкторы и методы — определять несколько конструкторов, либо несколько методов с одинаковым именем, отличающихся только типами параметров. (В случае конструкторов это единственный способ создать много конструкторов класса.) В качестве parameterTypes нужно отдать массив объектов типа Class, соответствующих типам всех параметров конструктора или метода. Разница между вариантами getXXX и getDeclaredXXX здесь та же самая, что и в случае методов получения списков. Здесь я бы порекомендовал написать небольшой тест,

55


программирование который распечатает списки всех конструкторов, полей и методов (объявленных и унаследованных) какого-нибудь класса. Для преобразования объектов Constructor, Field, Method в строки можно использовать стандартный метод toString(). Попробуйте распечатать такие списки для классов без конструкторов, с пустым конструктором, с конструктором, обладающим параметрами, с private- и publicполями. Попробуйте унаследовать класс и переопределить в нем (под тем же именем) public-, protected- или private-поле. Такой текст хорошо помогает понять, как в Java устроены классы, конструкторы и наследование. Как реально работать с классами Constructor, Field, Method? Прежде всего, заметим, что все они реализуют общий интерфейс Member, позволяющий: n получить символьное имя члена (конструктора, поля или метода) с помощью метода: public String getName();

n получить обратную ссылку на класс, в котором объявлен данный член (т.е. тот класс, который возвращает этот член в соответствующем массиве getDeclaredXXX()), с помощью метода: public Class getDeclaringClass();

n получить набор битовых флагов — модификаторов данного члена с помощью метода: public int getModifiers();

Модификаторы — это битовые флаги, описывающие, обладает ли данный член следующими свойствами: public, private, protected, static, final, synchronized, volatile, transient, native, abstract или strictfp. Полный список модификаторов и средства работы с ними можно найти в классe java.lang.reflect.Modifier — см. документацию фирмы Sun. Кстати заметим, что некоторые модификаторы (например, public) определены также у классов; их можно получить методом Class.getModifiers(). Для классов определен еще один модификатор — java.lang.reflect.Modifier.INTERFACE, означающий, что объект Class ассоциирован не с классом, а с интерфейсом. Главное назначение классов Constructor, Field, Method — конечно, получить доступ к соответствующим членам, т.е. в случае конструкторов — создать экземпляр класса, в случае полей — прочитать или изменить поле, в случае методов — вызвать метод. Это делается достаточно просто. Чтобы не дублировать документацию по Java, я просто приведу пример готового тестового класса, где использованы все основные приемы доступа к членам: import java.lang.reflect.*; public class TestClass { public int a; public TestClass(int a) {this.a= a;} public void b() {a= 1;} public void b(int p1) {a= p1;} public String toString() {return a+"";}

56

public static void main(String[] args) throws Exception { Class clazz= TestClass.class; Constructor c= clazz.getConstructor(new Class[] {int.class}); Object o= c.newInstance(new Object[] {new Integer(23)}); Field f= clazz.getField("a"); System.out.println(f.getInt(o)); f.setInt(o,24); System.out.println(o); Method m= clazz.getMethod("b",new Class[] {}); m.invoke(o,new Object[] {}); System.out.println(o); m= clazz.getMethod("b",new Class[] {int.class}); m.invoke(o,new Object[] {new Integer(2)}); System.out.println(o); } }

Создание экземпляра объекта с помощью конструктора похоже на вызов метода Class.newInstance(). Но в данном случае можно легко создать экземпляр класса, не обладающий конструктором без параметров — нужно только знать список типов параметров требуемого конструктора. Для передачи параметров в конструкторы и методы используется массив объектов (типа Object[]). Если нужно передать параметр примитивного типа, он «заворачивается» в соответствующий класс-оболочку — в случае int это объект Integer. Такое «заворачивание» — традиционная практика в мире отражений. Для доступа к полю в классе Field реализованы общие методы get() и set(), рассчитанные на произвольный объект, и частные версии getBoolean(), getInt(), ..., setBoolean(), setInt(), ..., рассчитанные на примитивный тип поля. Первым параметром у этих методов всегда передается экземпляр объекта, к полю которого нужно получить доступ. Для вызова метода служит метод invoke() класса Method. Ему передается экземпляр объекта, метод которого следует вызвать, и список параметров в виде массива Object[]. Если метод класса возвращает результат (а не описан как void, как в данном примере), этот результат будет возвращен в качестве результата invoke — в виде общего типа Object. (Примитивный тип в этом случае, как обычно, «заворачивается» в класс-оболочку.) Располагая объектом Constructor или Method, можно узнать список типов всех параметров в виде массива Class[]: для этого служат методы Constructor.getParameterTypes() и Method.getParameterTypes(). В случае метода можно также узнать тип результата: Method.getReturnType(). Кстати, это та самая экзотическая ситуация, когда находит применение исключительно редкий объект void.class (я упоминал об этой объекте в разделе 2): void.class будет возвращен getReturnType(), если метод объявлен как void. С помощью классов Field и Method можно также получить доступ к статическим полям и методам класса, даже не создавая экземпляра объекта. При этом в качестве первого параметра методов getXXX(), setXXX() или invoke() допускается передать null. Можно, скажем, легко написать Java-код, аналогичный по действию стандартной утилите «java» — а именно, запускающий статическую функцию «public static void main(String[] args)» у произвольного класса. Техника интерфейсов, описанная в предыдущем разделе, не позволила бы это сделать.


программирование Обход защиты Java В очень многих учебниках по Java подчеркивается, что модификаторы protected и private вместе с «дружественным» уровнем доступа (отсутствие модификаторов) позволяют обеспечить «100-процентную защиту» ваших полей и методов от использования посторонними классами. Например, вы никогда не сможете добраться до поля private char value[];

объявленного в исходном коде класса String и представляющего реальное содержимое строки. На самом деле все это не так. Смотрите, как можно добраться до этого самого поля и «нелегально» изменить строку — вопреки известному утверждению, что тип String является абсолютно неизменяемым: import java.lang.reflect.*; public class HackString { public static void main(String[] args) throws Exception { String s= «Hello!»; System.out.println(s); Field f= s.getClass().getDeclaredField(«value»); // Èìåííî getDeclaredField, à íå getField: // ïîñëåäíèé ìåòîä ïðîñòî íå íàøåë áû ñêðûòîãî ïîëÿ f.setAccessible(true); char[] value= (char[])f.get(s); value[5]= ? ; System.out.println(s); } }

Здесь «магический» метод — setAccessible(). Этот метод (и симметричный getAccessible()) имеется у всех классов Constructor, Field, Method и предназначен специально для того, чтобы отключить стандартную проверку модификаторов, осуществляемую Java-машиной. (Естественно, все это сработает только при условии, что в вашей версии Sun Java SDK реализация класса String точно так же основана на private-поле «char value[]». Так как это поле скрытое, фирма Sun вправе в любой момент переименовать его или вообще заменить чем-нибудь другим.) Спрашивается — зачем же это сделано? И разве это не является брешью в системе безопасности? Что до второго вопроса — разумеется, метод setAccessible() контролируется менеджером безопасности Java (так же как, например, работа с файлами), и никакая мало-мальски защищенная Java-система не позволит злоупотребить подобной возможностью. А чтобы понять, зачем это нужно, взгляните на любую среду разработки Java-проектов — скажем, NetBeans или JavaBuilder. Традиционная возможность подобных сред — показать все поля и методы некоторого класса, например, визуальной компоненты — в том числе и скрытые, а в некоторых случаях — дать возможность отредактировать значения полей. Язык Java уникален в том отношении, что подобные действия можно легко выполнить совершенно законными средствами самого языка, не прибегая, скажем, к анализу исходного текста программы. На самом деле неограниченный доступ ко всем конструкторам, полям и методам даже гораздо более ценен. Этот механизм мира отражений дает возможность удобно и естественно реализовать чрезвычайно мощные технологии, нереализуемые (или крайне сложно реализуемые) другими способами.

№1, октябрь 2002

Приведем два примера. Во-первых, в Java поддерживается механизм сериализации. Достаточно реализовать в вашем классе пустой интерфейс-индикатор java.io.Serializable, и появляется возможность полностью (т.е. со всеми полями и вложенными объектами) записать этот объект в поток java.io.ObjectOutputStream и впоследствии прочитать из потока java.io.ObjectInputStream. Те, кто изучал механизм сериализации Java, согласятся — во многом этот механизм напоминает черную магию. Каким-то образом классы java.io.ObjectInputStream и java.io.ObjectOutputStream, без всяких дополнительных подсказок со стороны разработчика класса, «догадываются», как записать или прочитать все поля объекта, включая private-поля. Более того, для «подсказки», когда она все же требуется, используются private-методы writeObject() и readObject() — классы java.io.ObjectOutputStream и java.io.ObjectInputStream каким-то образом обнаруживают и вызывают эти методы. Конечно же, в действительности эта «черная магия» — не что иное, как магия отражений. Описанные выше технологии, в том числе метод setAccessible(), в принципе, позволяют даже разработать свою собственную схему сериализации, отличную отстандартной, предлагаемой фирмой Sun. Второй пример — технология RMI. Это чрезвычайно мощный механизм, позволяющий распределять вычисления по сети — так, чтобы с точки зрения кода Java вызов метода выглядел, как обычно, а на самом деле этот метод отрабатывался на другом компьютере. Здесь тоже не обойтись без технологий отражения. Только они позволяют динамически превратить объект (со всеми своими полями) в поток данных, передать его по сети, реконструировать объект на другом компьютере и вызвать нужный метод — и все это скрыто от пользователя класса. Конечно, все это не повод, чтобы использовать отражения не по назначению для «взлома» защиты Java. Отражения следует использовать только тогда, когда без этого нельзя обойтись — иначе вы потеряете все преимущества, которые дает идеология объектно-ориентированного программирования.

Заключение Я постарался описать самые, на мой взгляд, важные и интересные аспекты технологии отражений. Статья — не справочник и не учебник. Многие вещи «остались за бортом». Не все методы классов были описаны; некоторые специфичные классы, такие как java.lang.reflect.Proxy, я вообще не рассматривал. Чтобы получить полную и точную информацию, всегда лучше обращаться к первоисточнику — документации фирмы Sun. Кроме сайта http://java.sun.com, документацию почти всегда можно найти в комплекте поставки Java или извлечь из комментариев к исходным текстам фирмы Sun. Я также хотел бы порекомендовать книгу: «Язык Программирования Java», К.Арнолд, Дж.Гослинг, Д.Холмс, Издательский дом «Вильямс», Москва — С.-Петербург — Киев, 2001. Это наиболее грамотная из попадавшихся мне книг на русском языке. Она написана сотрудниками фирмы Sun, участвовавшими в разработке технологии Java — т.е. в некотором роде является первоисточником.

57


программирование

COLDFUSION ИЛИ,

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

возможно, лучшее решение для создания Александр Меженков динамических сайтов Эта статья – первая в серии, адресованной читателям, делающим первые шаги в программировании на ColdFusion, а также тем, кто все еще не определился с выбором средств, позволяющих создавать динамические сайты, управляемые данными. Сколько раз вам приходилось слышать или самим задавать вопросы типа “как я могу сохранить информацию из HTML-форм на своих Web-страницах?” или “как мне сделать счетчик посещений?” или, наконец, “как мне защитить код своих Web-страниц от всеобщего обозрения?” В самом языке HTML нет никаких простых способов обработки данных HTML-форм. Броузер может лишь собрать информацию из форм и передать ее на Web-сервер. Обработка информации может быть возложена на CGI серверные расширения. Одно название чего стоит! А ведь их еще нужно написать и отладить своими руками, потратив немало времени на изучение различных стандартов и протоколов! Безусловно, это полезное, с точки зрения общего развития, занятие. Кто из нас не проходил на первых курсах института сопромат или теорию машин и механизмов, с тем, чтобы позже получить диплом программиста или специалиста по микропроцессорам?! Так ли уж это было необходимо? Пара слов пояснений для тех, кто не совсем в курсе. Хотя бы для того, чтобы понять от чего Вас может уберечь ColdFusion. CGI, или Common Gateway Interface, - это стандартный шлюзовый интерфейс. Проще говоря – это некоторый стандарт или набор правил, определяющий порядок общения

(взаимодействия, обмена данными, если угодно) между Web-сервером и прикладными программами, выполняющими ту или иную задачу. По существу, написанные вами CGI программы расширяют возможности Web-сервера, дополняя его нужной вам функциональностью. Отсюда, собственно и второе название CGI программ – серверные расширения. В принципе, CGI программа может быть написана на любом языке: C/C++, Perl, TCL, Visual Basic, Clipper, Fortran. Все зависит от того, какая у вас система и в какой среде программирования вы чувствуете себя более комфортно. Главное, чтобы ваша программа обеспечивала средства общения с Web-сервером, или, говоря иначе, удовлетворяла стандарту CGI. Естественно, если вы пишите на С, то перед запуском вам нужно скомпилировать программу. Если же вы используете один из языков-сценариев типа Perl, TCL или Unix shell, то все, что вам надо сделать перед запуском, это поместить файлы в каталог /cgi-bin, где по умолчанию их будет искать Web-сервер. В чем же недостаток CGI программ? Во-первых, не каждый специалист по электронной торговле или Web-дизайнер знает С или Visual Basic. Во-вторых, CGI программы слишком расточительны по отношению к системным ресурсам. При каждом обращении на вашу страницу и, соответственно, при каждом запуске CGI программы, система порождает новый поток, выделяя для него в оперативной памяти компьютера новое пространство. И если у вашего сайта много посетителей (а ведь именно для этого вы и создаете сайт), то легко может сложиться ситуация, когда в памяти сервера одно-


программирование временно будет находиться много копий одной и той же CGI программы, что, в свою очередь, очень быстро исчерпает всю оперативную. Наконец, при создании CGI программ немало усилий будет затрачено на программирование рутинных задач ввода/вывода, вместо того чтобы направить в созидательное русло решения прикладных задач сайта. Однако вернемся к теме статьи... Вначале немного истории. В далеком (по меркам Сети) 1995 году два брата Дж.Дж. И Джереми Эллейр (J.J. И Jeremy Allaire) в США (ну а где же еще?) основали новую компанию «Allaire Corporation» для продвижения первого в мире сервера Web-приложений, который они назвали ColdFusion. Одной из причин создания ColdFusion как раз и была сложность создания сайтов, управляемых данными с помощью CGI программ. Перед одним из братьев – Джереми – стояла задача периодического обновления электронной версии издававшегося в печатном виде журнала. Занятие это было крайне утомительным, и Джереми обратился к своему брату-программисту с просьбой написать для него какое-нибудь приложение, которое избавило бы его от излишней траты времени и сил, и позволило бы ему сосредоточиться на основной задаче – собственно, обновлению электронной версии журнала. Когда проект был завершен, оба брата осознали, что они испекли горячий пирожок, который наверняка многим придется по вкусу. Недолго думая, братья основали новую компанию и, не мудрствуя лукаво, дали ей свою фамилию. Вложив в свое детище 18 тысяч долларов личных сбережений, шестью годами позже, в 2001 году, братья Эллейр продали его корпорации Macromedia более чем за 360 миллионов долларов. У вас еще осталось желание писать низкоуровневые процедуры ввода/вывода? Что же представляет собой этот чудесный продукт, который принес своим создателям такую прибыль? Безусловно, вам знакомы термины hardware и software. Так вот, ColdFusion – это middleware. Термином middleware называют программное обеспечение, осуществляющее некоторые преобразования. В самом общем смысле сервер приложений ColdFusion является посредником, преобразующим ваш код, написанный на языке высокого уровня, называемом CFML (ColdFusion Markup Language) в теги HTML-документа, который может отобразить Web-броузер. Официальное

название ColdFusion – сервер Web-приложений, но в зависимости от того, как вы решите его использовать, он может быть средством разработки Web-страниц, сервером баз данных или вашим счастливым билетом в благополучную жизнь. От версии к версии ColdFusion предлагал все больше возможностей. Версия 1.5 в 1996 году содержала всего лишь 35 тегов, обеспечивавших простейшие функции доступа к базам данных и поддержки электронной почты. Сегодня 5-я версия ColdFusion предоставляет более 80 тегов и 255 функций для решения практически любой задачи, которая может возникнуть в Web-программировании. ColdFusion позволяет начать создание нового приложения на основе прочного фундамента развитого и полностью ориентированного на Web-среду языка программирования, обеспеченного серьезной поддержкой солидной компании (Macromedia), ее бизнес - партнерами и тысячами разработчиков, публикующих свои решения на специальном Web-ресурсе под названием Developers exchange. Этот ресурс настолько богат, что прежде, чем приступать к разработке решения какой–бы то ни было задачи, имеет смысл заглянуть сюда. Очень часто здесь можно найти совершенно бесплатно готовое решение. Конечно, на рынке существует много других технологий, которые вы можете использовать для создания динамических Web-приложений. Диапазон их достаточно широк: от “open source” технологий, таких как Perl и PHP, до коммерческих Java Server Pages (JSP) или Microsoft Active Server Pages (ASP). При таком богатстве выбора, что нас может побудить использовать ColdFusion, который, кстати сказать, является коммерческим и весьма недешевым продуктом. Его цена больше $1,000. Но пусть вас это не смущает. Во-первых, его покупают, как правило, не частные лица, а фирмы. А во-вторых, практика показывает, что стоимость конечного продукта или, говоря другими словами, “стоимость владения” (Total Cost of Ownership), включающая, помимо цены инструментальных средств, стоимость разработки алгоритмов, кодирования и отладки, часто оказывается ниже, чем стоимость приложения, разработанного с помощью “бесплатных” средств. Деньги, вложенные в ColdFusion окупаются очень быстро. Одной из главных причин, побуждающих, выбирать ColdFusion в качестве рабочего инструмента является легкость разработки. В отличие от большинства упоминавших-


программирование ся технологий, вам не нужно быть гуру программирования для того, чтобы начать работу с ColdFusion и успешно завершить свой первый проект, который приятно удивит вашего шефа. Эта простота использования отнюдь не означает отсутствия мощи и функциональной гибкости. Просто благодаря усилиям программистов из Allaire, а теперь и Macromedia, многие сложные вещи происходят за сценой. ColdFusion упрощает решение большинства задач, таких как обработка данных форм или выполнение запросов баз данных. Однако когда у вас возникает нужда выполнения более сложных операций, ColdFusion предоставляет вам такие возможности. В компаниях, использующих ColdFusion, сложные задачи, сборку всего приложения и его отладку осуществляет, как правило, действительно классный высокооплачиваемый специалист, тогда как простые операции возлагаются на новичков, которые еще только учатся программированию. Другим важным достоинством ColdFusion является наличие версий для всех популярных систем и Web-серверов. Неважно, на чем вы работаете: Windows 95/98/NT/2000, Solaris, Linux или HP-UX. ColdFuison совместим с большинством известных Web-серверов: Netscape Enterprise и iPlanet, Microsoft IIS и PWS, O’Reilly Website, Apache. Вы можете переносить ColdFusion приложения с платформы на платформу и легко переключаться между различными системами управления базами данных. Наиболее близким конкурентом ColdFusion является технология ASP. Какими, качествами кроме межплатформенной совместимости, может еще похвастать ColdFusion? Легкость и простота. Языком ASP является VBScript – подмножество Visual Basic, который сложнее ColdFusion. Сравним, например, два фрагмента кода. Вначале на ASP (VBScript): 1. 2. 3. 4. 5. 6.

<% Dim RandomFraction Randomize RandomFraction = Rnd Response.Write(RandomFraction) %>

Теперь то же самое на ColdFusion: 1. 2.

<cfset RandomFraction = Rand()> <cfoutput>#RandomFraction#</cfoutput>

Что проще – решать вам. При использовании ASP одной распространенной проблемой является необходимость закрывать базы данных после окончания работы с ними. Часто программисты забывают это делать. Конечно, если все делать без ошибок, эта проблема не возникает. Но покажите мне программиста, который пишет без ошибок. Подобная забывчивость приводит к тому, что пространство памяти, занимаемое объектом базы данных, не освобождается. Страницы с таким кодом, обращение к которым происходит несколько раз в минуту, весьма интенсивно расходуют оперативную память, что очень скоро может привести к зависанию сервера. ColdFusion отслеживает подобные ситуации автоматически. В части функциональных возможностей (без привлечения продуктов сторонних фирм) ColdFusion также дает некоторую фору ASP.

60

Вот лишь несколько из них: работа с почтой, встроенные HTTP, POP и FTP клиенты, встроенный поисковый механизм. Это ни в коем случае не агитация в пользу ColdFusion, просто объективная попытка познакомить с малоизвестным в России пакетом. Технология ASP, в свою очередь, также имеет преимущества. Но это уже тема другой статьи. За названием ColdFusion на самом деле скрываются два понятия: собственно сервер Web-приложений ColdFusion и язык программирования ColdFusion, называемый CFML (ColdFusion Markup Language). Файлы приложения ColdFusion имеют расширение .cfm и называются шаблонами (templates). Шаблоны ColdFusion наряду с тегами и функциями CFML могут содержать обычные HTML-теги и встроенные JavaScript и/или VBScript сценарии. CFML часть шаблона обрабатывается сервером приложений, в результате чего динамически генерируется часть выходной страницы основанная на данных формы, базы данных и так далее, которая позже объединяется со статической его частью, представленной HTML-тегами и сценариями JavaScript/VBScript в единый выходной HTML-документ, передаваемый Web-серверу. Рассмотрим, как ColdFusion обрабатывает пользовательские запросы. 1. Web-броузер направляет запрос к Web-серверу с требованием открыть файл ColdFusion. Эти файлы имеют расширение .cfm. 2. Получив запрос, Web-сервер перенаправляет его серверу приложений ColdFusion. 3. Сервер приложений анализирует шаблон и выполняет действия, предписанные встретившимися тегами и функциями CFML, взаимодействуя, если надо, с другими службами и приложениями, например источниками данных или почтовым сервером. В результате динамически создается часть результирующей HTML страницы. 4. Далее сервер приложений собирает воедино только что созданную динамическую часть страницы со статической частью исходного шаблона и возвращает результирующую страницу Web-серверу. 5. Web-сервер отправляет полученную от сервера ColdFusion страницу на пославшую запрос клиентскую машину. В заключение приведем полезные ссылки, а также примеры сайтов, построенных с использованием ColdFusion. 1. Îôèöèàëüíàÿ ñòðàíèöà îïèñàíèÿ ñòàíäàðòà CGI http://www.w3.org/CGI/ 2. Äîêóìåíòàöèÿ è ïðèìåðû CGI ïðîãðàìì http://hoohoo.ncsa.uiuc.edu/cgi/ 3. Ïðîãðàììíûå ïðîäóêòû Macromedia - íûíåøíåãî âëàäåëüöà ColdFusion. Îòñþäà æå ìîæíî ñêà÷àòü ïðîáíûå (trial)âåðñèè http://www.macromedia.com/software/ 4. Developer's exchange. Êîëëåêöèÿ ãîòîâûõ ðåøåíèé íà ColdFusion http://devex.macromedia.com/developer/gallery/index.cfm 5. ColdFusion ôîðóìû http://webforums.macromedia.com/coldfusion/ 7. On-line äîêóìåíòàöèÿ ïî ColdFusion http://www.macromedia.com/support/coldfusion/documentation.html


программирование


программирование Недавно я занимался разработкой утилитки, позволяющей запускать с сервера по сети некоторые программы на определённое время. При этом обмен происходил через UDP-сокеты. Для написания клиентской части необходимы были требования, чтобы она работала под управлением Windows 2000, и обычный непривилегированный пользователь не мог выключить или включить её.

ПРОГРАММИРОВАНИЕ СЕРВИСОВ В WINDOWS 2000 ВСЕВОЛОД СТАХОВ 62


программирование Для этой цели идеально подходят сервисы — служебные программы, выполняющиеся в фоновом режиме и без возможности завершения процесса через диспетчер задач. Для управления сервисами используется админис тративный компонент Windows NT — services.msc (находится в папке «Администрирование» панели управления). Как сервисы организовано множество системных служб Windows, таких как сетевые клиенты и серверы, утилиты управления оборудованием (пресловутый Plug and Play), а также драйверы ядра. Из компонента управления сервисами можно изменять режим работы сервисов. Сервис характеризуется состоянием: запущен, остановлен, приостановлен. Для управления сервисами используются соответствующие кнопки: запустить, остановить, приостановить (доступно лишь для некоторых сервисов), перезапустить. Можно сделать, чтобы некоторые сервисы запускались при запуске компьютера. Для этого существует три режима запуска сервиса: автоматический (auto) — сервис запускается при входе в систему вручную, (manual) — сервис запускается по требованию пользователя или системы, отключено (disabled) — сервис вообще не будет запускаться. Для изменения режима запуска откройте диалог свойств сервиса и на вкладке «Общие» выберите нужный режим. На данной вкладке также показано имя и описание сервиса (их можно изменить), а также путь к исполняемому файлу сервиса. Кроме того, можно изменять и другие параметры сервиса. Для этого смотрите встроенную справку Windows. При любых манипуляциях с сервисами учтите, что изменение базы данных сервисов доступно только администраторам. Кроме этого, возможна модификация сервисов на членах NT домена через MMC (Microsoft Management Console — mmc.exe) пользователем, имеющим права администратора домена. Создание сервиса в ОС Windows — задача нетривиальная. Например, создание демона в ОС Unix намного проще, хотя, я думаю, система сервисов лучше продумана и более централизована. Для создания нового сервиса, прежде всего, его нужно зарегистрировать в базе данных сервисов. Для этого открывается база данных сервисов функцией OpenSCManager для записи-создания, далее добавляется сам сервис функцией CreateService. После этого система может запустить приложение, зарегистрировавшее себя сервисом, но запуск идёт несколько необычным образом: запускается не WinMain, а ServiceMain, которая определяет обработчики событий сервиса (таких как запуск, пауза, остановка) функцией RegisterServiceCtrlHandler, устанавливает текущее состояние сервиса SetServiceStatus и выполняет функцию StartService для каждого зарегистрированного сервиса (их может быть несколько). Немного сложновато, не так ли? Но реально создать сервис ещё сложнее, так как данные функции принимают очень много параметров, например функция CreateProcess принимает аж 13 (!) параметров. Но я всё же попытаюсь вкратце рассказать о каждой функции и приведу конкретный пример сервиса. Итак, начнём с

№1, октябрь 2002

функции открытия базы данных сервисов: SC_HANDLE OpenSCManager (const char *MachineName, const char* DatabaseName, DWORD Desired Access);

Функция возвращает дескриптор базы данных сервисов с именем DatabaseName, если данный параметр равен NULL, то используется база данных по умолчанию (для этой же цели можно использовать константу SERVICES_ACTIVE_DATABASE) на компьютере MachineName или на локальном компьютере, если данный параметр NULL. Параметр DesiredAccess определяет режим доступа к файлу. Обычно используются константы GENERIC_READ, для чтения GENERIC_WRITE для создания новых сервисов или изменения параметров старых и GENERIC_EXECUTE разрешение на выполнение сервисов. При ошибке возвращается NULL, иначе — дескриптор базы данных. Далее происходит регистрация сервиса функцией CreateService, которая возвращает дескриптор сервиса для использования в данном процессе: SC_HANDLE CreateService( SC_HANDLE hSCManager, LPCTSTR lpServiceName, LPCTSTR lpDisplayName, DWORD dwDesiredAccess, DWORD dwServiceType, DWORD dwStartType, DWORD dwErrorControl, LPCTSTR lpBinaryPathName, LPCTSTR lpLoadOrderGroup, LPDWORD lpdwTagId, LPCTSTR lpDependencies, LPCTSTR lpServiceStartName, LPCTSTR lpPassword );

Итак, поподробнее о параметрах, так как я не думаю, что их названия говорят сами за себя. SC_HANDLE hSCManager — дескриптор базы данных сервисов, полученный функцией OpenSCManager. LPCTSTR lpServiceName — строка (до 256 символов), реальное имя сервиса в базе данных. LPCTSTR lpDisplayName — данный параметр — имя сервиса, которое показывается в инструменте управления сервисами. DWORD dwDesiredAccess — флаг доступа к сервису. Может принимать любые значения доступа. Обычно используются константы GENERIC_READ, GENERIC_WRITE, GENERIC_EXECUTE, а также значение SERVICE_ALL_ACCESS для предоставления полного доступа к сервису. DWORD dwServiceType — флаг типа сервиса, то есть то, как он будет выполняться системой. Возможные значения: SERVICE_WIN32_OWN_PROCESS — сервис существует в виде отдельного процесса; SERVICE_WIN32_SHARE_PROCESS — сервис разделяет процесс под названием services с другими сервисами; SERVICE_WIN32_KERNEL_DRIVER — сервис явля-

63


программирование ется драйвером ядра (то есть сам является частью ядра); SERVICE_WIN32_FILE_SYSTEM_DRIVER — сервис представляет собой драйвер файловой системы. Практически все сервисы, не являющиеся системными, создаются как собственные процессы SERVICE_WIN32_OWN_PROCESS. В этом случае, если необходимо, чтобы сервис взаимодействовал с рабочим столом, можно указать через битовое “или” флаг SERVICE_INTERACTIVE_PROCESS. DWORD dwStartType — флаг способа запуска сервиса, может принимать следующие значения: SERVICE_BOOT_START — служба запускается загрузчиком, NT то есть при загрузке ядра (допустимо только при создании драйвера ядра); SERVICE_SYSTEM_START — сервис запускается после загрузки ядра при инициализации драйверов (это также допустимо только для драйверов); SERVICE_AUTO_START — служба запускается при входе в систему; SERVICE_DEMAND_START — сервис запускается другим приложением (в том числе инструментом управления службами); SERVICE_DISABLED — сервис не может быть запущен. Для стандартных служб обычно применяются флаги SERVICE_AUTO_START и SERVICE_DEMAND_START для, соответственно, автоматической или ручной загрузки. DWORD dwErrorControl — флаг поведения при ошибке. Для большинства сервисов (кроме критичных и системных драйверов) разумно писать SERVICE_ERROR_IGNORE для игнорирования ошибки или SERVICE_ERROR_NORMAL для вывода сообщения о невозможности запуска (также добавляется запись в системный журнал). Для драйверов при невозможности их загрузки обычно происходит откат системы и (или) перезагрузка. LPCTSTR lpBinaryPathName — полный путь к исполняемому файлу сервиса. Учтите, что если путь содержит пробелы или является очень длинным, то писать его нужно в кавычках внутри строки, то есть “\”d:\\my folder\\my_super service.exe\». LPCTSTR lpLoadOrderGroup — имя группы сервиса, обычное значение — NULL, для указания, что сервис не принадлежит никакой группе. LPDWORD lpdwTagId — этот параметр содержит тег группы, которые применяются драйверами для идентификации внутри группы, обычное значение — NULL. LPCTSTR lpDependencies — массив строк-зависимостей сервиса. Если службы, от которых зависит данный сервис не запустились, то не произойдёт и запуска данного сервиса. Данный параметр помогает выстроить иерархическую структуру служб. LPCTSTR lpServiceStartName — имя пользователя для запуска сервиса. Имя вводится в формате Имя_домена\имя_пользователя (или .\имя_пользователя для локального домена). Если данный параметр NULL, то используется системный профиль LocalSystem, что подходит для большинства сервисов. LPCTSTR lpPassword — параметр, определяющий пароль для выбранного профиля. Если параметр равен NULL, то счи-

64

тается, что пароль — пустая строка. Для профиля локальной системы LocalSystem пароль всегда должен быть NULL. Итак, если вы дочитали, до этого момента, то всё остальное должно показаться Вам детскими играми. Кстати, сообщу информацию, которая многим покажется полезной: после регистрации сервиса его параметры записываются в системный реестр Windows в улей HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services \имя_сервиса. Для людей, любящих копаться в настройках ОС сообщаю: большинство системных драйверов являются сервисами и их настройки можно поменять в реестре. Например, очень интересным является ключ Tcpip — можно менять очень много “скрытых” настроек сети (включая настройки сетевых карт!). Для поклонников безопасной сети ес ть ещё один ключик: Lanmanserver — позволяет прикрыть некоторые дыры безопасности, например NetBios NULL session. Но вернёмся к реальности — к созданию сервисов. Чтобы еще больше отпугнуть читателя, приведу пример создания функции CreateService: CreateService (hsSManager,lpServiceName, lpDisplayName, dwDesiredAccess, dwServiceType, dwStartControl, lpBinaryPathName, lpLoadOrderGroup, lpdwTagId, lpDependencies, lpServiceStartName, lpPassword);

После создания сервиса считайте, что ваше дело завершено – смело пишите CloseServiceHandle (SC_HANDLE sh) и выходите из программы. Тут я сделаю небольшое лирическое отступление. Как известно, в ОС Windows после загрузки программы в память вызывается специализированная функция main для консольных приложений и WinMain для приложений GUI Windows. Программа, работающая как служба, ведёт себя несколько по-другому: вызов сервиса происходит функцией StartService(SC_HANDLE sh, DWORD param_count, char **parameters), после чего он загружается в память (если ещё не был загружен) и вызывается функция ServiceMain(DWORD param_count, char **parameters), которая также ведёт себя очень хитро, но об этом чуть позднее. Если для регистрации и для самого сервиса используется одна и та же программа, то обычно считывают аргументы командной строки. При этом используется следующая схема: #include <windows.h> int main(int argc, char **argv){ SC_HANDLE sh; SC_HANDLE svdb; // Äåñêðèïòîð áàçû ñåðâèñîâ SERVICE_TABLE_ENTRY DispatchTable[] = { { «MyService», MyServiceStart }, { NULL, NULL } }; if(argc < 2){ // Çàïóñê ñåðâèñà {

// Çàïóñê áåç àðãóìåíòîâ

if (!StartServiceCtrlDispatcher( DispatchTable))


программирование write_to_log(«Can`t execute service»); } }

if(argc == 2){ // Ïåðåäàí îäèí ïàðàìåòð if(strcmp(argv[1], -i )) install_service(); // Óñòàíîâêà ñåðâèñà åñëè àðãóìåíò -i if(strcmp(argv[1], -u )) uninstall_service(); // Óäàëåíèå ñåðâèñà åñëè àðãóìåíò -u } else{ write_to_log( Bad usage ); return -1; } return 0; }

В принципе, всё не так уж и сложно, хотя я кое-чего не рассказал. Во-первых, функция OpenService служит для загрузки сервиса в память. Функция принимает три параметра: дескриптор базы сервисов, имя сервиса и тип доступа – тут всё понятно. Для удаления сервиса используется функция DeleteService(SC_HANDLE sh), которая возвращает ноль в случае ошибки. При использовании данной функции учтите, что при открытии сервиса функцией OpenService, вы должны указать в правах доступа единственный флаг DELETE для удаления его из базы. Для запуска самого сервиса используется функция StartServiceCtrlDispatcher(LPSERVICE_TABLE_ENTRY lpServiceStartTable). Функция принимает единственный аргумент — массив строк, содержащий две строки — имя сервиса и имя функции-обработчика, заканчивается список двумя пустыми строками. После этого начинается обработка функции сервиса ServiceMain. Итак, начало самого интересного — написание функции ServiceMain. Данная функция служит для запуска, инициализации и изменения статуса сервиса. Поведение данной функции строго регламентировано: во-первых, она должна заполнить поля струк т уры SERVICE_STATUS значениями параметров данного сервиса; во-вторых — зарегистрировать обработчик событий сервиса функцией RegisterServiceCtrlHandler; и, втретьих — выполнить действия по инициализации сервиса и ус тановить сос тояние сервиса функцией SetServiceStatus. Итак, обо всём по порядку. Думаю, отдельного описания заслу живает струк т ура SERVICE_STATUS: struct SERVICE_STATUS { DWORD dwServiceType; ýòî ïîëå îçíà÷àåò òî æå, ÷òî è â ôóíêöèè CreateService, ò.å. òèï ïðèëîæåíèÿ ñåðâèñà(îòäåëüíûé, äðàéâåð ÿäðà, äðàéâåð ÔÑ). DWORD dwCurrentState; à âîò ýòî ñïåöèôè÷åñêîå ïîëå ñîäåðæèò òåêóùåå ñîñòîÿíèå ñåðâèñà, èìåííî åãî äîëæíà óñòàíàâëèâàòü ServiceMain. Äîïóñòèìûå çíà÷åíèÿ: SERVICE_STOPPED - ñåðâèñ îñòàíîâëåí; SERVICE_START_PENDING - ñåðâèñ çàïóñêàåòñÿ; SERVICE_STOP_PENDING - ñåðâèñ îñòàíàâëèâàåòñÿ; SERVICE_RUNNING - ñåðâèñ óæå çàïóùåí; SERVICE_CONTINUE_PENDING - ñåðâèñ ïðîäîëæàåò ðàáîòó; SERVICE_PAUSE_PENDING - ñåðâèñ ïåðåõîäèò â ðåæèì ïàóçû; SERVICE_PAUSED - ñåðâèñ íàõîäèòñÿ â ðåæèìå ïàóçû.

№1, октябрь 2002

DWORD dwControlsAccepted; áèòîâàÿ ìàñêà, ñîäåðæàùàÿ äîïóñòèìûå ñîñòîÿíèÿ ñåðâèñà(÷åðåç ïîáèòíîå «èëè»). Äîïóñòèìûå êîíñòàíòû: SERVICE_ACCEPT_STOP - ñåðâèñ ìîæåò áûòü îñòàíîâëåí; SERVICE_ACCEPT_PAUSE_CONTINUE - ñåðâèñ ìîæåò áûòü ïîñòàâëåí è ñíÿò ñ ïàóçû; SERVICE_ACCEPT_SHUTDOWN - ñåðâèñ áóäåò îïîâåù¸í ïðè âûõîäå èç ñèñòåìû. DWORD dwWin32ExitCode; ýòîò ïàðàìåòð ñîîáùàåò ñèñòåìå çíà÷åíèå, êîòîðîå âîçâðàùàåò ñåðâèñ ïðè îøèáêå, ïîäðîáíåå îøèáêà îïðåäåëÿåòñÿ ñëåäóþùèì ïàðàìåòðîì. DWORD dwServiceSpecificExitCode; êîíêðåòíàÿ îøèáêà, ïðîèçîøåäøàÿ â ñåðâèñå. DWORD dwCheckPoint; à ýòî ïîëîæåíèå ïðîãðåññ-áàðà ïðè çàïóñêå-îñòàíîâêå ñåðâèñà, èñïîëüçóåòñÿ äëÿ âèçóàëèçàöèè ïðîöåññà çàïóñêà ñåðâèñà. DWORD dwWaitHint; âðåìÿ â ìèëëèñåêóíäàõ, êîòîðîå æä¸ò âûçûâàþùàÿ ïðîãðàììà äî èçìåíåíèÿ ëèáî òåêóùåãî ñòàòóñà, ëèáî dwCheckPoint. Åñëè ýòîãî íå ñëó÷èëîñü, òî ñ÷èòàåòñÿ, ÷òî çàïóñê ñåðâèñà áûë íåóäà÷åí. Åñëè äàííîå çíà÷åíèå íîëü, òî óáèåíèÿ íå ïðîèñõîäèò. };

Итак, обычно вначале устанавливается значение статуса SERVICE_START_PENDING, затем устанавливается обработчик сообщений, инициализация сервиса и только после этого статус сервиса устанавливается в SERVICE_RUNNING. При данных действиях не забудьте корректно установить dwWaitHint, иначе система сочтёт, что ваш сервис не успел запуститься, и замочит его без сожаления. Для регистрации обработчика событий используется функция SERVICE_STATUS_HANDLE RegisterServiceCtrlHandler(LPCTSTR service_name, LPHANDLER_FUNCTION handler_function) — первый параметр — имя сервиса, второй — указатель на функцию-обработчик, данная функция возвращает дескриптор состояния сервиса для функции SetServiceStatus или NULL в случае ошибки. Для установки текущего состояния сервиса используется функция SetServiceStatus (SERVICE_STATUS_HANDLE ssh, LPSERVICE_STATUS status) — первый параметр — дескриптор состояния, а второй — указатель на структуру статуса. После этого приведу простенький пример главной функции: void MyServiceStart (DWORD argc, LPTSTR *argv) { DWORD status; DWORD specificError; SERVICE_STATUS MyServiceStatus; //Ýòà òà ñàìàÿ ñòðóêòóðà ñòàòóñà // À âîò ìû å¸ çàïîëíÿåì MyServiceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS; MyServiceStatus.dwCurrentState = SERVICE_START_PENDING; MyServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP; MyServiceStatus.dwWin32ExitCode = 0; MyServiceStatus.dwServiceSpecificExitCode = 0; MyServiceStatus.dwCheckPoint = 0; MyServiceStatus.dwWaitHint = 5000; //Ðåãèñòðèðóåì îáðàáîò÷èê ñîáûòèé MyServiceStatusHandle = RegisterServiceCtrlHandler(«MyService», MyServiceCtrlHandler); if (MyServiceStatusHandle == (SERVICE_STATUS_HANDLE)0)

65


программирование {

write_to_log(«Can not register handler function»); return; }

ñòâèÿ.

//À òåïåðü âûïîëíÿåì íåêîòîðûå èíèöèàëèçàöèîííûå äåé-

status = MyServiceInitialization(argc,argv, &specificError); // Ïðîâåðÿåì, êàê ïðîøëà èíèöèàëèçàöèÿ if (status != NO_ERROR) { //Îøèáêà - îñòàíàâëèâàåì ñåðâèñ è âûõîäèì MyServiceStatus.dwCurrentState = SERVICE_STOPPED; MyServiceStatus.dwCheckPoint = 0; MyServiceStatus.dwWaitHint = 5000; MyServiceStatus.dwWin32ExitCode = status; MyServiceStatus.dwServiceSpecificExitCode = specificError; SetServiceStatus (MyServiceStatusHandle, &MyServiceStatus); return; } // Âñ¸ ïðîøëî óñïåøíî :) èä¸ì äàëüøå. MyServiceStatus.dwCurrentState = SERVICE_RUNNING; MyServiceStatus.dwCheckPoint = 0; MyServiceStatus.dwWaitHint = 0; if (!SetServiceStatus (MyServiceStatusHandle, &MyServiceStatus)) {

status = GetLastError(); write_to_log(«Can not set service status 8-(«);

} //Ñåðâèñ íà÷àë ðàáîòàòü.

66

write_to_log(«Yeah, this works!»,0); }

return;

Прекрасная функция, не так ли? Ну и для последнего штриха расскажу ещё про обработчик сообщений. Если Вы когда-нибудь писали GUI программу, то, наверное, знаете, что обработчики событий — это длинная функция, которая получает номер события и выполняет в соответствии с этим некие действия. Так, в сервисах наблюдается нечто подобное. Формат функции должен быть такой: void WINAPI Handler(DWORD fdwControl); параметр fdwControl содержит текущее сообщение от системы. Возможные значения этого параметра: SERVICE_CONTROL_STOP — cервис должен остановиться. StartServiceCtrlDispatcher (LPSERVICE_TABLE_ENTRY lpServiceStartTable) SERVICE_CONTROL_PAUSE — cервис должен перейти в режим паузы. SERVICE_CONTROL_CONTINUE — cервис должен выйти из режима паузы. SERVICE_CONTROL_INTERROGATE — cервис должен сообщить о себе информацию. SERVICE_CONTROL_SHUTDOWN — cистема собирается выключаться. Ну вот, в принципе, и всё. Для дальнейшей информации Вы можете обратиться к MSDN. Но для большинства сервисов этой информации оказывается достаточно. Итак, дерзайте!


СЕТИ


сети

АНАЛИЗАТОР СЕТЕВОГО ТРАФИКА Введение Если Вы - системный администратор, специалист по безопасности, или Вам просто интересно, что происходит в Вашей локальной сети, то перехват и анализ нескольких сетевых пакетов может быть полезным упражнением. При помощи небольшой программы на языке С и базовых знаний сетевых технологий Вы сможете перехватить данные сетевого траффика, даже если они адресованы не Вам. В данной статье мы рассмотрим, как это можно сделать в сети Ethernet - наиболее распространенной на данный момент технологии построения локальных компьютерных сетей.

Обзор технологии Ethernet Для начала вспомним, как функционирует сеть Ethernet (те из вас, кто уже знаком с данным вопросом, могут пропустить этот параграф). IP-пакеты (дейтаграммы), источником которых является приложение пользователя, инкапсулируются в Ethernetкадры (пакеты Ethernet-протокола, передаваемые в сети). Каждый кадр содержит исходный IP-пакет и другую информацию, необходимую для доставки его адресату, в частности, 6-ти байтовый Ethernet-адрес (MAC-адрес) назначения, который при помощи протокола ARP ставится в соответствие IP-адресу назначения. Таким образом, сформированный кадр, содержащий пакет, начинает свое путешествие от хоста-отправителя к хосту-получателю через кабельное соединение.

68

На уровне протокола Ethernet маршрутизация отсутствует. Другими словами, кадр, отправленный хостом-отправителем, не попадает напрямую хосту-получателю, а будет доступен для всех хостов, объединенных в сеть. Каждая сетевая карта принимает кадр и считывает из него первые 6 байт. Эти байты содержат MAC-адрес хоста-получателя, но только одна карта в сети определит его как свой собственный и передаст кадр для дальнейшей обработки сетевому драйверу. Сетевой драйвер проверит поле «Тип протокола» заголовка кадра и, основываясь на этом значении, направит инкапсулированный пакет соответствующей приемной функции данного протокола. В большинстве случаев это протокол IP. Приемная функция изымает IP заголовок из принятой дейтаграммы и передает инкапсулированное сообщение соответствующему модулю протокола транспортного уровня (например, TCP или UDP). Эти протоколы, в свою очередь, обрабатывают свои заголовки и передают данные протоколам прикладного уровня. В течение этой «экскурсии» по различным уровням сетевого стека исходный пакет теряет все служебные поля протоколов и, в конце концов, данные, передаваемые в пакете, принимаются пользовательским приложением.

Пакетные сокеты При создании сокета стандартным вызовом socket (int domain, int type, int protocol) параметр domain определяет коммуникационный домен, в котором будет использовать-

ся сокет. Обычно используются значения PF_UNIX для соединений, ограниченных локальной машиной, и PF_INET для соединений, базирующихся на протоколе IPv4. Аргумент type определяет тип создаваемого сокета и имеет несколько значений. Значение SOCK_STREAM указывается при создании сокета для работы в режиме виртуальных соединений (протокол TCP), а значение SOCK_DGRAM - для работы в режиме пересылки дейтаграмм (протокол UDP). Последний параметр protocol определяет используемый протокол (в соответствии с IEEE 802.3). В версиях LINUX, начиная с 2.2, появился новый тип сокетов - пакетные сокеты. Пакетные сокеты используются для отправления и приема пакетов на уровне драйверов устройств. Сокеты данного типа создаются вызовом socket (SOCK_PACKET, int type, int protocol). Параметр type равен или SOCK_RAW, или SOCK_DGRAM. Пакеты типа SOCK_RAW передаются драйверу устройства и принимаются от него без всяких изменений данных пакета. SOCK_DGRAM работает на более высоком уровне. Физический заголовок (MACадрес) удаляется перед тем, как пакет отправляется на обработку пользователю.

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


сети фейса (IP-адрес, маску подсети, номер подсети, размер MTU, индекс (номер) интерфейса); - создать пакетный сокет и привязать его к определенному интерфейсу; - принять сетевой пакет и проанализировать его структуру. Этим будет заниматься главная функция программы. Для удобства каждый из пунктов оформим в виде отдельной функции или заголовочного файла.

Переменные Необходимые заголовочные файлы: #include #include #include #include #include #include #include #include

<stdio.h> <sys/types.h> <sys/socket.h> <errno.h> <linux/if.h> <linux/if_ether.h> <linux/in.h> <linux/ip.h>

Структура для хранения принятого IP пакета: struct ip_packet { struct iphdr ip; char *ip_data; } ip_pack;

Cтруктура для хранения параметров сетевого интерфейса:

Функция получения параметров сетевого интерфейса #include «ip.h» #include <sys/ioctl.h> int getifconfig (struct ifreq *ifr, char *intf, struct ifparam *ifp) {

int fd; - дескриптор сокета. Создадим сокет: if (( fd= socket (AF_INET, SOCK_DGRAM, 0)) <0 ) { perror ( «socket» ); return ( - 1 ); }

Скопируем имя интерфейса в поле ifr_name структуры ifr: sprintf (ifr->ifr_name, «%s», intf);

Получим IP адрес интерфейса: if (ioctl (fd, SIOCGIFADDR, ifr) <0 ) { perror («ioctl»); return (-1); } memset(&s, 0, sizeof (struct sockaddr_in)); memcpy(&s, &ifr->ifr_addr, sizeof (struct sockaddr)); memcpy(&ifp-ip, &to.sin_addr.s_addr, sizeof (u_long));

Получим маску подсети: if (ioctl (fd, SIOCGIFNETMASK, ifr) <0 ) { perror («ioctl»); return (-1); } memset(&s, 0, sizeof (struct sockaddr_in)); memcpy(&s, &ifr->ifr_netmask, sizeof (struct sockaddr)); memcpy(&ifp-mask, &to.sin_addr.s_addr, sizeof (u_long));

Получим номер подсети: ifp->sunbet = check_subnet(ifp->mask, ifp->ip);

Получим размер MTU: if (ioctl (fd, SIOCGIFMTU, ifr) <0 ) { perror («ioctl»); return (-1); } ifp -> mtu = ifr -> ifr_mtu;

Получим индекс (номер) интерфейса: if ( ioctl (fd, SIOCGIFINDEX, ifr) <0 ) { perror («ioctl»); return (-1); } ifp -> index = ifr -> ifr_ifindex;

Переведем интерфейс в неразборчивый режим. Для этого получим значение

struct ifreq ifr;

Структура для получения адресной информации: struct sockaddr_in s;

Структура для хранения заголовка IPпакета: struct iphdr *ip;

Структура для хранения заголовка Ethernet-кадра: struct ethhdr eth;

Вспомогательная структура, содержащая параметры интерфейса: struct ifparam { u_long ip; u_long mask; u_long subnet; int mtu; int index; } ifp;

int e0_r, - дескриптор сокета; rec; - размер принятого пакета в байтах; char *buff; - буфер для приема пакетов. Переменные разместим в файле ip.h.

№1, октябрь 2002

69


сети флагов интерфейса: if ( ioctl (fd, SIOCGIFFLAGS, ifr) <0 ) { perror («ioctl»); close (fd); return (-1); }

Установим флаг неразборчивого режима: ifr -> ifr_flags |= IFF_PROMISC;

Установим новое значение флагов интерфейса: if ( ioctl (fd, SIOCSIFFLAGS, ifr) <0 ) { perror («ioctl»); close (fd); return (-1); } return 1; }

Параметрами функции getifconfig являются структура struct ifreq *ifr, имя интерфейса char *intf и структура struct ifparam *ifp. Номер подсети определяется при помощи вызова функции check_subnet, приведенной ниже. Установкой флага интерфейса IFF_PROMISC мы добиваемся приема всех пакетов, даже если они не адресованы нашему хосту.

Функция определения номера подсети BITS 32 GLOBAL check_subnet SECTION .text check_subnet: push ebp - ñîõðàíèì àäðåñ âîçâðàòà èç ôóíêöèè mov ebp, esp mov edx, [ebp+8] - ïåðâûé ïàðàìåòð - ìàñêà ïîäñåòè mov eax, [ebp+12] - âòîðîé ïàðàìåòð - IP àäðåñ mov cx, 32 - ÷èñëî ðàçðÿäîâ â IP àäðåñå â ôîðìàòå IPv4 push cx - ñîõðàíèì çíà÷åíèå â ñòåêå xor esi, esi - îáíóëèì ñ÷åò÷èê .label bt edx, esi - ñêàíèðóåì ìàñêó â ïîèñêàõ 1 jnc .msk - âûõîä èç öèêëà ïðè ñîâïàäåíèè inc esi - èíêðåìåíò ñ÷åò÷èêà loop .label - ïðîäîëæèòü ïîèñê .mask pop cx - èçâëå÷ü ðàíåå ñîõðàíåííîå çíà÷åíèå èç ñòåêà sub cx, si - ÷èñëî ðàçðÿäîâ â àäðåñå, îòâåäåííûõ ïîä õîñòîâóþ ÷àñòü shl eax, cl - ëîãè÷åñêèé ñäâèã íà ýòî çíà÷åíèå mov esp, ebp pop ebp - âîññòàíîâèì ñòåê ret - âîçâðàò èç ôóíêöèè

70

Функция check_subnet сканирует первый переданный параметр (маску подсети) до первого появления 1 и запоминает эту позицию. Эта позиция соответствует числу разрядов, отведенных в адресе на сетевую составляющую. Далее, во втором переданном параметре (адресе) происходит логический сдвиг на число разрядов, отведенных под адрес хоста. Таким образом, у нас остается только сетевая составляющая, которая и является адресом подсети.

Функция для создания пакетного сокета Заголовочные файлы: #include <sys/types.h> #include <sys/socket.h> #include <errno.h > #include <linux/if_packet.h> #include <linux/if_ether.h> int getsock_recv (int index) { int fd;

Структура для хранения адресной информации об интерфейсе (см. файл <linux/ if_packet.h>): struct sockaddr_ll

s_ll;

Создадим пакетный сокет: if (( fd= socket (SOCK_PACKET, SOCK_RAW, htons (ETH_P_ALL) )) <0 ) { perror ( «socket» ); return ( - 1 ); }

Выделим память для структуры struct sockaddr_ll s_ll: memset (&s_ll, 0, sizeof (struct sockaddr_ll));

Заполним поля структуры s_ll необходимыми значениями: s_ll.sll_family = PF_PACKET; - òèï ñîêåòà s_ll.sll_protocol = htons (ETH_P_ALL); - òèï ïðèíèìàåìîãî ïðîòîêîëà s_ll.sll_ifindex = index; - íîìåð èíòåðôåéñà s_ll.sll_pkttype = PACKET_HOST; - òèï ïàêåòà (äëÿ ëîêàëüíîé ìàøèíû)

Привяжем сокет к интерфейсу: if ((bind (fd, (struct sockaddr *) &s_ll, sizeof (struct sockaddr_ll)) <0 ) { perror («bind»); close (fd); return (-1); }

Возвратим дескриптор сокета в вызывающую функцию:

}

return (fd);

Функция getsock_recv принимает в качестве параметра индекс интерфейса и возвращает дескриптор пакетного сокета. Значение поля protocol в системном вызове socket равно htons (ETH_P_ALL). Это означает, что мы будем принимать пакеты всех протоколов. Все входящие пакеты будут передаваться пакетному сокету до того, как они будут переданы протоколам, реализованным в ядре. Список возможных протоколов приведен в файле <linux/ if_ether.h>. Для получения пакетов только с определенного интерфейса используется функция bind: таким образом мы соединяем пакетный сокет с интерфейсом, адрес которого указан в структуре struct sockaddr_ll. Если этого не сделать, мы будем получать пакеты со всех сетевых интерфейсов, которые в данный момент активны.

Главная функция #include «ip.h» int main () {

Получим параметры сетевого интерфейса: memset (&ifr, 0, sizeof (struct ifreq)); if (getifconfig (&ifr, «eth0», &ifp) <0 ) { perror («getifconfig»); exit (1); }

Выделим память: buff = (char *) malloc (ifp.mtu + 18); memset(&ip, 0, sizeof (struct ip_packet)); ip_pack.ip_data = (char *) malloc ( ifp.mtu - sizeof (struct iphdr)); ip=(struct iphdr *)&ip_pack.ip;

Получим дескриптор пакетного сокета: if ((e0_r = getsock_recv (ifp.index)) <0 ) { perror («getsock_recv»); exit(1); }

Цикл приема пакетов: for (;;) { Îáíóëèì áóôåð: bzero (buff, ifp.mtu+18); rec = 0;

Принять пакет:

rec=recvfrom (e0_r, (char *)buff, ifp.mtu+18, 0, NULL, NULL); if (rec<0) { perror («recvfrom»); exit(1); }


сети Число принятых байт (длина принятого пакета): printf («\nrec = %d\n», rec);

Первые 12 байт в принятом буфере содержат MAC - адреса отправителя и получателя. Заполним структуру struct ethhdr eth адресной информацией: memcpy ((char *) &eth, buff, 12);

По смещению 14 в данном буфере расположены данные Ethernet-кадра - IP-пакет: memcpy ((char *)&ip.pack, (buff + 14), ifp.mtu );

Проведем анализ принятого Ethernetкадра. continue;

if ((ip -> version) !=4)

версия IP-протокола MAC-адрес отправителя: printf (« %.2x: %.2x: %.2x: %.2x: %.2x: %.2x \t -> \t «, eth.h_source[0], eth.h_source[1], eth.h_source[2], eth.h_source[3], eth.h_source[4], eth.h_source[5]);

MAC-адрес получателя: printf (« %.2x: %.2x: %.2x: %.2x: %.2x: %.2x», eth.h_dest[0], eth.h_ dest[1], eth.h_ dest[2], eth.h_ dest[3], eth.h_ dest[4], eth.h_ dest[5]); printf («%d \n», ip -> ihl); - äëèíà çàãîëîâêà IP-ïàêåòà printf («%d \n», ntohs (ip -> tot_len)); - äëèíà âñåãî ïàêåòà printf («%d \n», ip -> protocol); - ïðîòîêîë âåðõíåãî óðîâíÿ printf («%s \t -> \t»,

inet_ntoa (ip -> saddr)); - à ä ð å ñ èñòî÷íèêà printf («%s \n», inet_ntoa (ip -> daddr)); - àäðåñ íàçíà÷åíèÿ } }

return (1);

Прием пакетов осуществляется с помощью функции recvfrom. Эта функция принимает данные через дескриптор e0_r. Принятое сообщение копируется в структуру ip_pack. В принятом пакете первым следует заголовок Ethernet-кадра. По смещению 14 расположен IP-пакет. Поле «Версия» указывает тип данного пакета. Для IPv4-пакета данное поле содержит значение 4 в двоичной форме. Значение длины заголовка лежит в диапазоне между 20 и 60 байтами и находится в поле «Длина заголовка». Поле «Протокол» содержит идентификацию протокола следующего, более высокого уровня, содержащегося в разделе данных (т.е. в теле сообщения) данного IPпакета. В документе RFC 1700 перечислены все значения, которые могут содержаться в поле «Протокол» в заголовке IP-пакета. Поле «Адрес источника» и поле «Адрес назначения» содержат соответственно IPадрес отправителя пакета и IP-адрес предполагаемого получателя. Дальнейшая обработка принятого пакета зависит от полей «Длина заголовка» и «Протокол». В принятом буфере по смещению, указанном в поле «Длина заголовка» (с учетом заголовка кадра Ethernet) будет расположен заголовок протокола следующего уровня. Его анализ аналогичен вышеприведенному анализу заголовка IP. Для получения исполняемого модуля создадим Makefile следующего содержания:

# Êîìïèëÿòîð Ñ CC = gcc # Êîìïèëÿòîð àññåìáëåðà NASM = nasm # Èìÿ èñïîëíÿåìîãî ìîäóëÿ name = ip IP = ip.o check_snet.o getsock_recv.o getifconf.o $(name): $(IP) $(CC) -g -o $(name) $(IP) ip.o: ip.c $(CC) -c ip.c check_snet.o: check_snet.asm $(NASM) -f elf check_snet.asm getsock_recv.o: getsock_recv.c $(CC) -c getsock_recv.c getifconf.o: getifconf.c $(CC) -c getifconf.c clean: rm -f *.o

В файле ip.c находится главная функция программы. Командой make мы получим исполняемый модуль ip. Команда make clean удалит все объектные файлы. Результаты работы программы будут отображаться на консоли. Иногда это не совсем удобно, поэтому, немного модифицировав программу, результаты можно сохранять в файле. Компиляцию производим с ключем -g для возможности последующей отладки. Надеюсь, что читателю это не понадобится, но все-таки хочу показать простой прием поиска неисправностей в программе (не только в этой). Иногда программа, хотя и компилируется без ошибок, при запуске выдает сообщение Segmentation fault и завершается. Для нашего примера, если программа работает некорректно, запустите исполняемый файл ip на отладку командой gdb ip. В командной строке отладчика наберите run и изучите информацию, которую выдаст отладчик. Он укажет место, где программа аварийно завершилась, и Вам останется только устранить неисправность. Если все в порядке, то перекомпилируйте программу с ключем -s. В следующей статье мы рассмотрим, как можно передать принятый пакет, создав, тем самым, простейший шлюз.


сети

СКАНЕР ПОРТОВ: ПРИМЕР РЕАЛИЗАЦИИ

ВЛАДИМИР МЕШКОВ Введение Подавляющее большинство сетевых служб использует при работе протокол TCP. Согласно модели OSI, ТСР является протоколом транспортного уровня. Он обеспечивает надежное двунаправленное соединение между двумя процессами; данные передаются в обоих направлениях без ошибок, пакеты не теряются и не дублируются, последовательность передачи данных не нарушается.

72

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

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


сети Целью данной статьи является описание принципов функционирования и внутреннего устройства простого сканера портов TCP протокола.

Порядок установления TCP соединения Для понимания принципа работы сканера необходимо знать, каким образом устанавливается TCP соединение. При установлении соединения задействуются поля «Порядковый номер» (SEQ), «Номер подтверждения» (ACK-SEQ), флаги SYN, ACK и RST заголовка TCP-пакета. Флаг SYN является флагом синхронизации. Он используется при установлении соединения и устанавливает начальный порядковый номер, используемый для последующей передачи данных. Флаг ACK указывает на то, что поле номера подтверждения содержит достоверные данные. Флаг RST используется для сброса соединения. Соединение устанавливается в 3 этапа: n инициатор соединения (клиент) формирует SYN-пакет (TCP-пакет с установленным флагом SYN), заполняет поле SEQ и передает пакет серверу; n если порт, на который пришел запрос на соединение, открыт, сервер формирует SYN|ACK-пакет, заполняет поля SEQ, ACK-SEQ и передает пакет клиенту. Значение поля ACK-SEQ равно значению поля SEQ из пакета клиента, увеличенному на 1 (т.е. ACK-SEQ-сервера = SEQ-клиента + 1). Если порт закрыт, клиенту отправляется RSTпакет; n получив SYN|ACK-пакет, клиент проверяет поле ACK-SEQ и высылает серверу ACK-пакет. После этого соединение считается установленным и переходит в фазу обмена данными между клиентом и сервером.

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

№1, октябрь 2002

читать в статье «NESSUS - современный анализ безопасности, методы сканирования»(http://www.hack zone.ru/articles/nessus.htm#scan), а также в документации на сканер Nmap (http://www.insecure.org/nmap). Мы рассмотрим один из методов SYN-сканирование. Этот метод часто называют half-open (полуоткрытым) сканированием, т.к. полное TCP-соединение с портом сканируемой машины не устанавливается. Суть данного метода заключается в следующем. Вы посылаете TCP-пакет с установленным флагом SYN, как если бы собирались установить реальное соединение с выбранным портом, и ожидаете ответ. Принятый пакет с установленными флагами SYN и ACK указывает на то, что выбранный порт открыт и ожидает соединения. Флаг RST означает обратное. Если получен SYN|ACK-пакет, следует немедленно отправить пакет с флагом RST для сброса соединения, хотя реально за вас это сделает ядро. Преимуществом данной технологии является отсутствие в log-файлах сканируемой машины записей о попытках установления соединения с ней. Недостаток необходимость наличия прав root для формирования SYN-пакета.

Пример реализации сканера Приведенный ниже код был разработан и протестирован в ОС GNU/ Linux, дистрибутив Slackware 7.1, компилятор gcc-2.95.2. Алгоритм реализации следующий: n определить необходимые переменные и заголовочные файлы; n создать сокеты для приема и передачи пакетов; n сформировать SYN-пакет и отправить его сканируемому хосту; n принять ответный пакет и проанализировать состояние флагов SYN, ACK, RST; n сделать вывод о статусе проверяемого порта.

Заголовочные файлы и переменные Заголовочные файлы и переменные разместим в файле, который назовем scan.h. Для работы нам пона-

добятся следующие header-файлы: #include #include #include #include #include #include #include #include #include #include #include

<stdio.h> <stdlib.h> <errno.h> <sys/types.h> <sys/socket.h> <sys/ioctl.h> <linux/in.h> <linux/ip.h> <linux/tcp.h> <linux/if.h> <linux/if_ether.h>

структуры: struct ifreq *ifr - ñòðóêòóðà äëÿ õðàíåíèÿ ïàðàìåòðîâ ñåòåâîãî èíòåðôåéñà struct iphdr *ih - ñòðóêòóðà, ñîäåðæàùàÿ çàãîëîâîê IP-ïàêåòà sturct tcphdr *th - ñòðóêòóðà, ñîäåðæàùàÿ çàãîëîâîê TCP-ïàêåòà struct sockaddr_in local - ñòðóêòóðà, ñîäåðæàùàÿ àäðåñíóþ èíôîðìàöèþ î ëîêàëüíîé ñèñòåìå struct sockaddr_in dest - ñòðóêòóðà, ñîäåðæàùàÿ àäðåñíóþ èíôîðìàöèþ îá óäàëåííîé ñèñòåìå struct p_header { u_long s_addr; u_long d_addr; u_char zer0; u_char protocol; u_int lenght; } *pseudo - ïñåâäîçàãîëîâîê. Íåîáõîäèì ïðè ðàñ÷åòå êîíòðîëüíîé ñóììû TCPïàêåòà (âçÿò èç èñõîäíûõ òåêñòîâ ñêàíåðà Nmap)

и переменные: int fd - äåñêðèïòîð âñïîìîãàòåëüíîãî ñîêåòà e0_s- äåñêðèïòîð ñîêåòà äëÿ ïåðåäà÷è e0_r- äåñêðèïòîð ñîêåòà äëÿ ïðèåìà sent- ÷èñëî ïåðåäàííûõ áàéò rec- ÷èñëî ïðèíÿòûõ áàéò port- íîìåð ñêàíèðóåìîãî ïîðòà index- èíäåêñ èíòåðôåéñà, ÷åðåç êîòî ðûé îñóùåñòâëÿåòñÿ ñêàíèðîâàíèå. u_char *packet- ïàêåò, ïåðåäàâàåìûé â ñåòü.

Сокет для передачи Дескриптор сокета для передачи получим при помощи функции getsock_send, приведенной ниже. Необходимые заголовочные файлы: #include <errno.h> #include <sys/types.h> #include <sys/socket.h> #include <linux/in.h> #include <linux/if.h> #include <linux/if_ether.h> int getsock_send ( char *intf) - âûçîâ ôóíêöèè

Функция getsock_send принимает строковое значение (имя интерфейса) и возвращает дескриптор сокета в случае положительного завершения или -1, если произошла ошибка.

73


сети Переменные: int fd - äåñêðèïòîð ñîêåòà const int on=1 - ôëàã âêëþ÷åíèÿ çàãîëîâêà (ñì. íèæå) struct ifreq ifr - ñòðóêòóðà äëÿ õðàíåíèÿ ïàðàìåòðîâ ñåòåâîãî èíòåðôåéñà.

Сокет создадим следующим системным вызовом: if (( fd = socket ( AF_INET, SOCK_RAW, htons(ETH_P_IP) )) < 0 ) { perror ( «socket» ); return (-1); }

В данном и последующих вызовах мы будем включать код, обрабатывающий возможные ошибки. Комментировать его особого смысла нет, и так все понятно, надеюсь. Сокеты типа SOCK_RAW (RAWсокеты) домена AF_INET удобны тем, что позволяют получить непосредственный доступ к служебным полям протокола TCP/IP, в отличии от типов SOCK_STREAM и SOCK_DGRAM (для TCP и UDP соединений, соответственно). Существуют также пакетные сокеты, о которых я рассказывал в статье «Анализатор сетевого траффика», на них мы сейчас останавливаться не будем. Так как формировать пакеты мы будем вручную, то необходимо в опциях сокета указать данный факт. Делается это при помощи системного вызова setsockopt следующим образом: if ( setsockopt ( fd, IPPROTO_IP, IP_HDRINCL, ( const void *) &on, sizeof ( on ) ) < 0 ) { perror ( «setsockopt IP_HDRINCL» ); close ( fd ); return ( -1 ); }

Опция IP_HDRINCL является опцией протокола IP, поэтому параметр level вызова setsockopt равен IPPROTO_IP. Если опция IP_HDRINCL установлена, приложение строит и вставляет в исходящий пакет полный IP заголовок. Данная опция включается ненулевым значением (const int on=1). Поскольку мы собираемся работать через определенный интерфейс, необходимо осуществить привязку сокета к выбранному интерфейсу. Для этой цели также используется системный вызов setsockopt: sprintf ( ifr.ifr_name, «%s», intf );

74

if ( setsockopt ( fd, SOL_SOCKET, SO_BINDTODEVICE, (void *)&ifr, sizeof (ifr)) <0 ) { perror ( « SO_BINDTODEVICE» ); close ( fd ); return ( -1 ); }

Опция SO_BINDTODEVICE является опцией сокета, параметр level вызова setsockopt принимает значение SOL_SOCKET. Опция SO_BINDTODEVICE использует экземпляр структуры ifreq. Вызов setsockopt считывает из буфера данной структуры имя интерфейса, при помощи которого будут обслуживаться все доступы к рассматриваемому сокету. Поэтому вначале мы заполняем соответствующее поле структуры ifreq именем интерфейса, который был передан как параметр функции getsock_send. Если все успешно, возвращаем в главную функцию дескриптор сокета: return ( fd ).

Сокет для приема Дескриптор сокета для приема получим при помощи функции getsock_recv (пример данной функции был рассмотрен ранее в статье «Анализатор сетевого траффика»). Заголовочные файлы: #include #include #include #include #include

<sys/types.h> <sys/socket.h> <errno.h > <linux/if_packet.h> <linux/if_ether.h>

int getsock_recv (int index) - âûçîâ ôóíêöèè

Функция getsock_recv принимает в качестве параметра индекс интерфейса и возвращает дескриптор сокета. Дескриптор сокета: int fd;

Структура для хранения адресной информации об интерфейсе (см. файл <linux/if_packet.h>): struct sockaddr_lls_ll;

Создадим пакетный сокет: if (( fd= socket (SOCK_PACKET, SOCK_DGRAM, htons (ETH_P_ALL) )) <0 ) { perror ( «socket» );

}

return ( - 1 );

Пакетный сокет имеет тип SOCK_DGRAM. Это означает, что заголовок физического уровня (MACадрес в случае Ethernet) будет отброшен при приеме. Выделим память для структуры struct sockaddr_ll s_ll: memset (&s_ll, 0, sizeof (struct sockaddr_ll));

Заполним поля структуры s_ll необходимыми значениями: s_ll.sll_family = PF_PACKET; - òèï ñîêåòà s_ll.sll_protocol = htons (ETH_P_ALL); - òèï ïðèíèìàåìîãî ïðîòîêîëà s_ll.sll_ifindex = index; - íîìåð èíòåðôåéñà s_ll.sll_pkttype = PACKET_HOST; - òèï ïàêåòà (äëÿ ëîêàëüíîé ìàøèíû)

Для получения пакетов только с определенного интерфейса используется функция bind: таким образом мы соединяем пакетный сокет с интерфейсом, номер которого указан в структуре struct sockaddr_ll s_ll. Привяжем сокет к интерфейсу: if ((bind (fd, (struct sockaddr *) &s_ll, sizeof (struct sockaddr_ll)) <0 ) { perror («bind»); close (fd); return (-1); }

Возвратим дескриптор сокета в вызывающую функцию: return (fd)

Главная функция #include «scan.h» - ôàéë ñ ïåðåìåííûìè è header-ôàéëàìè int main ( int argc, char *argv [ ] ) - âûçîâ ãëàâíîé ôóíêöèè

Главной функции мы передаем два параметра: IP-адрес сканируемого хоста (аргумент argv[1]) и номер порта (аргумент argv[2]). Обработкой ошибочного ввода параметров будет заниматься функция usage (): void usage () { printf ( « \n scan [ dest_IP ] [ dest_port ] \n» ); return; }

Первое, что необходимо сделать


сети при запуске программы, это проверить, все ли необходимые параметры указаны: if ( argc != 3 ) { usage (); exit ( 1 ); }

Преобразуем введенные строковые значения адреса и порта в сетевой формат и заполним адресную структуру удаленной системы: port = atoi ( argv [2] ); memset ( &dest, 0, sizeof ( struct sockaddr_in )); dest.sin_addr.s_addr = inet_ntoa ( argv [1] ); dest.sin_port = htons ( port );

Тоже самое проделаем для локального хоста. memset ( &local, 0, sizeof ( struct sockaddr_in ));

Получим IP-адрес интерфейса и занесем его в адресную структуру local: );

p_header )); - îáíóëèì ñòðóêòóðó pseudo -> s_addr = local.sin_addr.s_addr; àäðåñ ëîêàëüíîãî õîñòà pseudo -> d_addr = dest.sin_addr.s_addr; àäðåñ óäàëåííîãî õîñòà pseudo -> protocol = 6; - ïðîòîêîë (TCP) pseudo -> lenght = htons ( sizeof ( struct tcphdr )); - äëèíà ïñåâäîçàãîëîâêà

Сформируем TCP заголовок. memset ( th, 0, sizeof ( struct tcphdr )); - îáíóëèì ñòðóêòóðó th -> source = local.sin_port; ëîêàëüíûé ïîðò th -> dest = dest.sin_port; - óäàëåííûé ïîðò th -> seq = htonl ( 1156270349 ); - íà÷àëüíûé ïîðÿäêîâûé íîìåð th -> ack_seq = 0; íîìåð ïîäòâåðæäåíèÿ th -> doff = 5; äëèíà çàãîëîâêà ( â 32-õ ðàçðÿäíûõ ñëîâàõ ) th -> syn = 1; óñòàíîâèòü ôëàã SYN th -> window = htons ( 3072 ); ðàçìåð îêíà th -> check = 0; îáíóëèòü ïîëå êîíòðîëüíîé ñóììû th -> check = in_cksum (( u_short *)pseudo, sizeof ( struct tcphdr) + sizeof ( struct p_header));

fd = socket ( AF_INET, SOCK_DGRAM, 0

sprintf ( ifr -> ifr_name, «%s», «eth0»); octl ( fd, SIOCGIFADDR, ifr ); memcpy (( char *) &local, ( char *)&( ifr -> ifr_addr ), sizeof ( struct sockaddr )); local.sin_port = htons (53);

Получим индекс интерфейса: ioctl ( fd, SIOCGIFINDEX, ifr ); index = ifr -> ifindex;

Выделим память для хранения данных, передаваемых и принимаемых из сети. Пакет будет состоять только из заголовков IP и TCP протоколов. packet = ( u_char * )malloc( sizeof ( struct iphdr ) + sizeof ( struct tcphdr ));

Внутри общего пакета разместим служебные заголовки IP и TCP протоколов, а также псевдозаголовок. in = ( struct iphdr * ) packet; th = ( struct tcphdr * ) (packet + sizeof ( struct iphdr )); pseudo = (struct p_header *) ( packet + sizeof ( struct iphdr) - sizeof ( struct p_header ));

Заполним поля псевдозаголовка необходимыми значениями. memset ( pseudo, 0, sizeof ( struct

№1, октябрь 2002

Алгоритм расчета контрольной суммы для заголовков TCP и IP одинаковый и будет изложен ниже. Сформируем IP заголовок. memset ( ih, 0, sizeof ( struct iphdr )); - îáíóëèì ñòðóêòóðó ih -> version = 4; - âåðñèÿ ïðîòîêîëà ih -> ihl = 5; äëèíà çàãîëîâêà (÷èñëî 32-õ áèòíûõ ñëîâ) ih -> tot_len = htons (sizeof (struct iphdr)+sizeof(struct tcphdr)); - äëèíà ïàêåòà ih -> id = 3290; - ïîðÿäêîâûé íîìåð ïàêåòà (èäåíòèôèêàöèÿ) ih -> ttl = 42; - âðåìÿ æèçíè ih -> protocol = 6; - òðàíñïîðòíûé ïðîòîêîë (TCP) ih -> saddr = local.sin_addr.s_addr; - ëîêàëüíûé àäðåñ ih -> daddr = dest.sin_addr.s_addr; - óäàëåííûé àäðåñ ih -> check = in_cksum (( u_short *) ih, sizeof (struct iphdr )); êîíòðîëüíàÿ ñóììà

Некоторые поля заголовков (например, поле «Начальный порядковый номер» TCP заголовка и поле «Идентификация» заголовка IP) были выбраны совершенно произвольно, т.к. в данном случае эти значения ни на что не влияют. Отобразим для контроля имеющуюся адресную информацию:

printf ( «IP-àäðåñ íàçíà÷åíèÿ \t -\t %s \n «, inet_ntoa ( ih -> daddr )); printf ( «IP-àäðåñ èñòî÷íèêà \t \t %s \n «, inet_ntoa ( ih -> saddr )); printf ( «Ïîðò íàçíà÷åíèÿ \t \t \t %d \n «, ntohs ( th -> dest )); printf ( «Ïîðò èñòî÷íèêà \t\t - \t %d \n «, ntohs ( th -> source ));

Создадим сокеты для передачи и приема пакетов. if (( e0_s = getsock_send ( «eth0» )) < 0 ) { perror ( «getsock_send» ); exit ( 1 ); } if (( e0_r = getsock_recv ( index )) < 0 ) { perror ( «getsock_recv» ); exit ( 1 ); }

Передадим сформированный SYN-пакет хосту назначения. dest.sin_family = AF_INET; sent = sendto ( e0_s, (char *) packet, ntohs ( ih -> tot_len), 0, ( struct sockaddr *)&dest, sizeof (struct sockaddr_in)); if ( sent <= 0 ) { perror ( «sendto» ); exit ( 1 ); } printf ( «\n Ïåðåäàíî %d áàéò \n», sent );

Примем ответ на наш запрос. Прием будем осуществлять в бесконечном цикле, каждый раз обнуляя приемный буфер. for ( ; ; ) { bzero ( packet, sizeof (packet)); rec = 0; rec = recvfrom ( e0_r, (char *) packet, sizeof ( struct iphdr ) + sizeof ( struct tcphdr ), 0, NULL, NULL ); if ( rec <0 || rec > 1500 ) { perror ( «recvfrom» ); exit ( 1 ); }

Число 1500 определяет максимальный размер MTU для сети Ethernet. Больше этого значения в одном пакете принять мы не можем, и любое превышение данного предела трактуется как ошибка. Теперь займемся анализом принятого пакета. Для начала проверим соответствие версии протокола IP. Поле «Версия» должно содержать 4. Если это не так (к нам мог поступить ARPзапрос, который мы не собираемся обрабатывать), то принятый пакет от-

75


сети брасывается и продолжается ожидание: if (( ih -> version ) != 4 ) continue;

Также мы не будем обрабатывать IP-пакеты, отправителем которых не является сканируемый хост: if (( ih -> saddr dest.sin_addr.s_addr ) continue;

!=

Контрольная сумма Расчет контрольной суммы выполняет следующая функция: #include < linux/types.h > __u16 in_cksum ( __u16 *ptr, int nbytes ) { register __u32 sum; __u16 oddbyte; register __u16 answer; sum = 0; while ( nbytes > 1 ) { sum += *ptr ++; nbytes -= 2; }

и если транспортный протокол не есть TCP: if (( ih -> protocol != 6 ) continue;

Если принятый пакет соответствует всем условиям, то нам останется только отобразить результаты: printf ( «Ïðèíÿòî %d áàéò \n \n «, rec ); printf ( «%s \t -> \t « , inet_ntoa ( ih ->saddr )); printf ( «%s \t \n \n «, inet_ntoa ( ih > daddr )); printf ( «Âåðñèÿ \t \t \t = %d \n», ih > version ); printf ( «Äëèíà çàãîëîâêà \t \t = %d \n», ih -> ihl ); printf ( «Äëèíà ïàêåòà \t \t= %d \n», ntohs (ih -> tot_len )); printf ( «Èäåíòèôèêàòîð \t \t = %d \n», ih -> id ); printf ( «Âðåìÿ æèçíè \t \t = %d \n», ih -> ttl ); printf ( «Ïðîòîêîë \t \t = %d \n», ih -> protocol ); printf ( «Êîíòðîëüíàÿ ñóììà IP \t= %d \n», ih -> check ); printf ( «Ïîðò èñòî÷íèê \t \t = %d \n», ntohs ( th -> source )); printf ( «Ïîðò íàçíà÷åíèÿ \t \t = %d \n», ntohs ( th -> dest )); printf ( «Êîíòðîëüíàÿ ñóììà TCP \t = %d \n», th->check); printf ( «SEQ \t \t \t = %lu \n», ntohl ( th -> seq )); printf ( «ACK-SEQ \t \t \t = %lu \n», ntohl ( th -> ack_seq )); if ( th -> syn == 1 ) printf ( «Ôëàã SYN óñòàíîâëåí \n» ); if ( th -> ack == 1 ) printf ( «Ôëàã ACK óñòàíîâëåí \n» ); if ( th -> fin == 1 ) printf ( «Ôëàã FIN óñòàíîâëåí \n» ); if ( th -> rst == 1 ) printf ( «Ôëàã RST óñòàíîâëåí \n» ); if ( th -> psh == 1 ) printf ( «Ôëàã PUSH óñòàíîâëåí \n» ); if ( th -> urg == 1 ) printf ( «Ôëàã URG óñòàíîâëåí \n» ); if (( th -> syn == 1 )&&(th->ack==1)) printf («Ïîðò %d îòêðûò \n», ntohs (th > source ));

Здесь все предельно ясно. После этого мы прерываем цикл приема пакетов и выходим из программы. break; } return (1); }

Сброс соединения возложим на ядро.

76

if ( nbytes == 1 ) { oddbytes = 0; * (( unsigned char *) &oddbyte ) = * (unsigned char *) ptr; sum += oddbyte; } & 0xFFFF);

}

sum = ( sum >> 16 ) + ( sum sum += (sum >> 16 ); answer=~sum; return (answer);

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

Makefile Для сборки выполняемого модуля создадим Makefile следующего содержания: CC = gcc name = scan SCAN = scan.o checksum.o getsock_send.o getsock_recv.o $( name ) : $( SCAN ) $( CC ) -g -o $( name ) $( NAME ) scan.o : scan.c $( CC ) -c scan.c checksum.o : checksum.c $( CC ) -c checksum.c getsock_send.o : getsock_send.c $( CC ) -c getsock_send.c getsock_recv.o : getsock_recv.c $( CC ) -c getsock_recv.c clean: rm -f *.o

Для получения исполняемого модуля достаточно ввести команду make. После этого в каталоге, где размещены файлы программы, появиться файл scan. При запуске в командной строке модуля необходимо указать IP-адрес сканируемого хоста и номер проверяемого порта. Вот в принципе и все. Обо всех замечаниях и пожеланиях пишите на ubob@mail.ru.


FAQ PERL Можно ли скомпилировать из Perl исполняемый файл? Вы можете воспользоваться программой Perl2Exe. Это утилита для преобразования Perl сценариев в выполняемые файлы, не требующие присутствия интерпретатора языка Perl. Perl2Exe может сгенерировать модули для Win32 и многих клонов Unix. Perl2Exe также позволяет Вам создавать не консольные программы, с использованием Tk. ht tp://www.indigostar.com/ perl2exe.htm — разработчик — IndigoSTAR Software. Еще один продукт IndigoSTAR Software — SendMail for Windows(TM) — Windows версия популярной программы Unix Sendmail. Она позволяет отправлять сообщений из командной строки, CGI сценария или BAT-файла.

Proc::Background — Общий ли для Unix и Win32 интерфейс управление фоновыми процессами? Это общий интерфейс для управления фоновыми процессами как на Unix, так и на Win32 платформах. Модуль позволяет Вам запускать и завершать фоновые процессы, получать выходные данные и отслеживать состояние фоновых процессов. P.S. Рекомендую при использовании под Win32 брать архив со CPAN и посмотреть прилагаемые примеры и скрипты. Proc::Background — http:// search.cpan.org/search?dist=ProcBackground

Как можно стандартизировать (оформить в виде процедуры) получение выборки из БД, чтобы получать набор записей с именованными полями? Это можно сделать так: sub QueryArrayOfHashes my ($DB, $query) = @_; my ($result,$data_hash,@items,$key, $val,%hash); $result = $DB->prepare($query); $result->execute or return; while ($data_hash=$result>fetchrow_hashref)

}

%hash=%$data_hash; push @items,{%hash}; } $result->finish; @items;

Комментарии: $data_hash - ññûëêà íà õýø %$data_hash == %{$data_hash} - ïîëó÷åíèå ñàìîãî õýøà èç ññûëêè {%hash} = ðàçèìåíîâàííûé õýø - ÷òîá ïîëó÷èëñÿ ìàññèâ õýøåé, à íå ïðîñòî îäèí ìàññèâ @items â äàííîì ñëó÷àå == return @items Ïðèìåð èñïîëüçîâàíèÿ: use DBI; ... $dbh=DBI->connect( DBI:mysql:mysql: localhost , $user, $password, {RaiseError => 1}) or die «connecting : $DBI::errstr\n»; @res = QueryArrayOfHashes($dbh, «select user, password from user»); for ($i=0; $i<=$#res; $i++) { print «\n[Record #$i]::\n»; foreach $key (sort keys %{$res[$i]}) { # çàïèñü âèäà $a[1]{b} ýêâèâàëåíòíà $a[1]->{b} print $key, «\t», $res[$i]{$key}, «\n»; } } $dbh->disconnect;

Как удалить дерево каталогов? В «Perl Cookbook» by Tom Christiansen and Nathan Torkington, O’Reilly («Библиотека программиста: Perl», издательство «Питер»), приводится два примера рекурсивного удаления каталога вместе с его содержимым. В одном используется функция finddepth из модуля File::Find, во втором - функция rmtree из File::Path. Вот еще один способ: # в качестве параметров скрипт принимает # список директорий для удаления die «usage: $0 <dir1> [<dir2> ... <dirN>]\n» unless @ARGV; foreach $path (@ARGV) { del_folder($path); } sub del_folder { my $dir=shift; return 0 unless $dir; my (@dirs,@files,$filename, $newdir,$list); opendir(DIR,$dir) or (warn «Can t rmdir $dir: $!» and return 0); @dirs=grep {!(/^\./) && -d «$dir/ $_»} readdir(DIR); rewinddir(DIR); @files=grep {!(/^\.(\.)?$/) && -f «$dir/$_»} readdir(DIR); closedir (DIR); for $list(0..$#dirs) { $newdir=$dir.»/».$dirs[$list]; del_folder($newdir); } for $list(0..$#files) { $filename=$dir.»/».$files[$list]; unlink $filename or (warn «Can t unlink $filename: $!» and next); } rmdir $dir or (warn «Can t rmdir $dir: $!» and return 0); return 1; }

по материалам www.xpoint.ru составил Дмитрий Горяинов

№1, октябрь 2002

77


ПРОГРАММИРОВАНИЕ СОКЕТОВ ВСЕВОЛОД СТАХОВ


сети Таким образом, сетевые сокеты представляют собой парные структуры, жёстко между собой синхронизированные. Для создания сокетов в любой операционной системе, поддерживающей их, используется функция socket (к счастью, сокеты достаточно стандартизированы, поэтому их можно использовать для передачи данных между приложениями, работающими на разных платформах). Формат функции таков: int socket(int domain, int type, int protocol);

Параметр domain задаёт тип транспортного протокола, т.е. протокола доставки пакетов в сети. В настоящее время поддерживаются следующие протоколы (но учтите, что для разных типов протокола тип адресной структуры будет разный): PF_UNIX èëè PF_LOCAL PF_INET

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

PF_INET6 PF_IPX

Ëîêàëüíàÿ êîììóíèêàöèÿ äëÿ ÎÑ Unix(è ïîäîáíûõ) IPv4, ip ïðîòîêîë Internet, íàèáîëåå ðàñïðîñòðàí¸í ñåé÷àñ(32-õ áèòíûé àäðåñ) IPv6, ñëåäóþùåå ïîêîëåíèå ïðîòîêîëà ip(IPng) - 128 áèòíûé àäðåñ IPX - ïðîòîêîëû Novell

Поддерживаются и другие протоколы, но эти 4 являются самыми популярными. Параметр type означает тип сокета, т.е. то, как будут передаваться данные: обычно применяется константа SOCK_STREAM, её использование означает безопасную передачу данных двунаправленным потоком с контролем ошибок. При таком способе передачи данных программисту не приходится заботиться об обработке ошибок сети, хотя это не уберегает от логических ошибок, что актуально для сетевого сервера. Параметр protocol определяет конкретный тип протокола для данного domain, например IPPROTO_TCP или IPPROTO_UDP (параметр type должен в данном случае быть SOCK_DGRAM). Функция socket просто создаёт конечную точку и возвращает дескриптор сокета; до того, как сокет не соединён с удалённым адресом функцией connect, данные через него пересылать нельзя! Если пакеты теряются в сети, т.е. произошло нарушение связи, то приложению, создавшему сокет, посылается сигнал Broken Pipe — SIGPIPE, поэтому целесообразно присвоить обработчик данному сигналу функцией signal. После того, как сокет соединён с другим функцией connect, по нему можно пересылать данные либо стандартными функциями read — write, либо специализированными recv — send. После окончания работы сокет надо закрыть функцией close. Для создания клиентского приложения достаточно связать локальный сокет с удалённым (серверным) функцией connect. Формат этой функции такой: int connect(int sock_fd, const struct *sockaddr serv_addr, socketlen_t addr_len);

При ошибке функция возвращает -1, статус ошибки

79


сети можно получить средствами операционной системы. При успешной работе возвращается 0. Сокет, однажды связанный, чаще всего не может быть связан снова, так, например, происходит в протоколе ip. Параметр sock_fd задаёт дескриптор сокета, структура serv_addr назначает удалённый адрес конечной точки, addr_len содержит длину serv_addr (тип socketlen_t имеет историческое происхождение, обычно он совпадает с типом int). Самый важный параметр в этой функции — адрес удалённого сокета. Он, естественно, неодинаков для разных протоколов, поэтому я опишу здесь структуру адреса только для ip(v4) протокола. Для этого используется специализированная структура sockaddr_in (её необходимо прямо приводить к типу sockaddr при вызове connect). Поля данной структуры выглядят следующим образом: struct sockaddr_in{ sa_family_t sin_family; îïðåäåëÿåò ñåìåéñòâî àäðåñîâ, âñåãäà äîëæíî áûòü AF_INET u_int16_t sin_port; ïîðò ñîêåòà â ñåòåâîì ïîðÿäêå áàéò struct in_addr sin_addr; ñòðóêòóðà, ñîäåðæàùàÿ ip àäðåñ }; Ñòðóêòóðà, îïèñûâàþùàÿ ip-àäðåñ: struct in_addr{ u_int32_t s_addr; ip àäðåñ ñîêåòà â ñåòåâîì ïîðÿäêå áàéò };

Обратите внимание на особый порядок байт во всех целых полях. Для перевода номера порта в сетевой порядок байт можно воспользоваться макросом htons (unsigned short port). Очень важно использовать именно этот тип целого — беззнаковое короткое целое. Адреса IPv4 делятся на одиночные, широковещательные (broadcast) и групповые (multicast). Каждый одиночный адрес указывает на один интерфейс хоста, широковещательные адреса указывают на все хосты в сети, а групповые адреса соответствуют всем хостам в группе (multicast group). В структуре in_addr можно назначать любой из этих адресов. Но для сокетных клиентов в подавляющем большинстве случаев присваивают одиночный адрес. Исключением является тот случай, когда необходимо просканировать всю локальную сеть в поисках сервера, тогда можно использовать в качестве адреса широковещательный. Затем, скорее всего, сервер должен сообщить свой реальный ip адрес и сокет для дальнейшей передачи данных должен присоединяться именно к нему. Передача данных через широковещательные адреса не есть хорошая идея, так как неизвестно, какой именно сервер обрабатывает запрос. Поэтому в настоящее время сокеты, ориентированные на соединение, могут использовать лишь одиночные адреса. Для сокетных серверов, ориентированных на прослушивание адреса, наблюдается другая ситуация: здесь разрешено использовать широковещательные адреса, чтобы сразу же ответить клиенту на запрос о местоположении сервера. Но обо всём по порядку. Как вы заметили, в структуре sockaddr_in поле ip адреса представлено как беззнаковое длинное целое, а мы привыкли к адресам либо в формате x.x.x.x

80

(172.16.163.89) либо в символьном формате (myhost.com). Для преобразования первого служит функция inet_addr (const char *ip_addr), а для второго — функция gethostbyname (const char *host). Рассмотрим обе из них: u_int32_t inet_addr(const char *ip_addr)

— возвращает сразу же целое, пригодное для использования в структуре sockaddr_in по ip адресу, переданному ей в формате x.x.x.x. При возникновении ошибки возвращается значение INADDR_NONE. struct HOSTENT* gethostbyname(const char *host_name)

— возвращает структуру информации о хосте, исходя из его имени. В случае неудачи возвращает NULL. Поиск имени происходит вначале в файле hosts, а затем в DNS. Структура HOSTENT предоставляет информацию о требуемом хосте. Из всех её полей наиболее значительным является поле char **h_addr_list, представляющее список ip адресов данного хоста. Обычно используется h_addr_list[0], представляющая первый ip адрес хоста, для этого можно также использовать выражение h_addr. После выполнения функции gethostbyname в списке h_addr_list структуры HOSTENT оказываются простые символические ip адреса, поэтому необходимо воспользоваться дополнительно функцией inet_addr для преобразования в формат sockaddr_in. Итак, мы связали клиентский сокет с серверным функцией connect. Далее можно использовать функции передачи данных. Для этого можно использовать либо стандартные функции низкоуровневого ввода/вывода для файлов, так как сокет — это, по сути дела файловый дескриптор. К сожалению, для разных операционных систем функции низкоуровневой работы с файлами могут различаться, поэтому надо посмотреть руководство к своей операционной системе. Учтите, что передача данных по сети может закончиться сигналом SIGPIPE, и функции чтения/записи вернут ошибку. Всегда нужно помнить о проверке ошибок, кроме того, нельзя забывать о том, что передача данных по сети может быть очень медленной, а функции ввода/вывода являются синхронными, и это может вызвать существенные задержки в работе программы. Для передачи данных между сокетами существуют специальные функции, единые для всех ОС — это функции семейства recv и send. Формат их очень похож: int send(int sockfd, void *data, size_t len, int flags); îòïðàâëÿåò áóôåð data int recv(int sockfd, void *data, size_t len, int flags); ïðèíèìàåò áóôåð data

Первый аргумент — дескриптор сокета, второй — указатель на данные для передачи, третий — длина буфера и четвёртый — флаги. В случае успеха возвращается число переданных байт, в случае неудачи — отрицательный код ошибки. Флаги позволяют изменить


сети параметры передачи (например, включить асинхронный режим работы), но для большинства задач достаточно оставить поле флагов нулевым для обычного режима передачи. При отсылке или приёме данных функции блокируют выполнение программы до того, как будет отослан весь буфер. А при использовании протокола tcp/ip от удалённого сокета должен прийти ответ об успешной отправке или приёме данных, иначе пакет пересылается ещё раз. При пересылке данных учитывайте MTU сети (максимальный размер передаваемого за один раз кадра). Для разных сетей он может быть разным, например, для сети Ethernet он равен 1500. Итак, для полноты изложения приведу самый простенький пример программы на Си, реализующей сокетного клиента: #include <sys/socket.h> /* Ñòàíäàðòíûå áèáëèîòåêè ñîêåòîâ äëÿ Linux */ #include <net/netinet.h> /* Äëÿ ÎÑ Windows èñïîëüçóéòå #include<winsock.h> */ #include <stdio.h> int main(){ int sockfd = -1; /* Äåñêðèïòîð ñîêåòà */ char buf[128]; /* Óêàçàòåëü íà áóôåð äëÿ ïðè¸ìà */ char s[] = "Client ready\n"; /* Ñòðîêà äëÿ ïåðåäà÷è ñåðâåðó */ HOSTENT *h = NULL; /* Ñòðóêòóðà äëÿ ïîëó÷åíèÿ ip àäðåñà */ sockaddr_in addr; /* Còðóêòóðà tcp/ip ïðîòîêîëà */ unsigned short port = 80; /* Çàïîëíÿåì ïîëÿ ñòðóêòóðû: */ addr.sin_family = AF_INET; addr.sin_port = htons(port); sockfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); /* Ñîçäà¸ì ñîêåò */ if(sockfd == -1) /* Ñîçäàí ëè ñîêåò */ return -1; h = gethostbyname("www.myhost.com"); /* Ïîëó÷àåì àäðåñ õîñòà */ if(h == NULL) /* À åñòü ëè òàêîé àäðåñ? */ return -1; addr.sin_addr.s_addr = inet_addr(h->h_addr_list[0]); /* Ïåðåâîäèì ip àäðåñ â ÷èñëî */ if(connect(sockfd, (sockaddr*) &addr, sizeof(addr))) /* Ïûòàåìñÿ ñîåäèíèòñÿ ñ óäàë¸ííûì ñîêåòîì */ return -1; /* Ñîåäèíåíèå ïðîøëî óñïåøíî - ïðîäîëæàåì */ if(send(sockfd, s, sizeof(s), 0) < 0) /* Ïîñûëàåì óäàë¸ííîìó ñîêåòó ñòðîêó s */ return -1; if(recv(sockfd, buf, sizeof(buf), 0) < 0) /* Ïîëó÷àåì îòâåò îò óäàë¸ííîãî ñåðâåðà */ return -1; printf("Recieved string was: %s", buf); /* Âûâîä áóôåðà íà ñòàíäàðòíûé âûâîä */ close(sockfd); /* Çàêðûâàåì ñîêåò */

}

/* Äëÿ Windows ïðèìåíÿåòñÿ ôóíêöèÿ closesocket(s) */ return 0;

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

№1, октябрь 2002

сокет, затем ему присваивается локальный адрес функцией bind, при этом можно присвоить сокету широковещательный адрес. Затем начинается прослушивание адреса функцией listen, запросы на соединение помещаются в очередь. То есть функция listen выполняет инициализацию сокета для приёма сообщений. После этого нужно применить функцию accept, которая возвращает новый, уже связанный с клиентом сокет. Обычно для серверов характерно принимать много соединений через небольшие промежутки времени. Поэтому нужно постоянно проверять очередь входящих соединений функцией accept. Для организации такого поведения чаще всего прибегают к возможностям операционной системы. Для ОС Windows чаще используется многопоточный вариант работы сервера (multi-threaded), после принятия соединения происходит создание нового потока в программе, который и обрабатывает сокет. В *nix системах чаще используется порождение дочернего процесса функцией fork. При этом накладные расходы уменьшены за счёт того, что фактически происходит копия процесса в файловой системе proc. При этом все переменные дочернего процесса совпадают с родителем. И дочерний процесс может сразу же обрабатывать входящее соединение. Родительский же процесс продолжает прослушивание. Учтите, что порты с номерами от 1 до 1024 являются привилегированными и их прослушивание не всегда возможно. Ещё один момент: нельзя, чтобы два разных сокета прослушивали один и тот же порт по одному и тому же адресу! Для начала рассмотрим форматы вышеописанных функций для создания серверного сокета: int bind(int sockfd, const struct *sockaddr, socklen_t addr_len);

— присваивает сокету локальный адрес для обеспечения возможности принимать входящие соединения. Для адреса можно использовать константу INADDR_ANY, которая позволяет принимать входящие соединения со всех адресов в данной подсети. Формат функции аналогичен connect. В случае ошибки возвращает отрицательное значение. int listen(int sockfd, int backlog);

— функция создаёт очередь входящих сокетов (количество подключений определяется параметром backlog, оно не должно превышать числа SOMAXCONN, которое зависит от ОС). После создания очереди можно ожидать соединения функцией accept. Сокеты обычно являются блокирующими, поэтому выполнение программы приостанавливается, пока соединение не будет принято. В случае ошибки возвращается -1. int accept(int sockfd, struct *sockaddr, socklen_t addr_len)

— функция дожидается входящего соединения (или извлекает его из очереди соединений) и возвращает новый сокет, уже связанный с удалённым клиентом. При

81


сети этом исходный сокет sockfd остается в неизменном состоянии. Структура sockaddr заполняется значениями из удалённого сокета. В случае ошибки возвращается -1. Итак, приведу пример простого сокетного сервера, использующего функцию fork для создания дочернего процесса, обрабатывающего соединение: int main(){ pid_t pid; /* Èäåíòèôèêàòîð äî÷åðíåãî ïðîöåññà */ int sockfd = -1; /* Äåñêðèïòîð ñîêåòà äëÿ ïðîñëóøèâàíèÿ */ int s = -1; /* Äåñêðèïòîð ñîêåòà äëÿ ïðè¸ìà */ char buf[128]; /* Óêàçàòåëü íà áóôåð äëÿ ïðè¸ìà */ char str[] = «Server ready\n»; /* Ñòðîêà äëÿ ïåðåäà÷è ñåðâåðó */ HOSTENT *h = NULL; /* Ñòðóêòóðà äëÿ ïîëó÷åíèÿ ip àäðåñà */ sockaddr_in addr; /* Còðóêòóðà tcp/ip ïðîòîêîëà */ sockaddr_in raddr; unsigned short port = 80; /* Çàïîëíÿåì ïîëÿ ñòðóêòóðû: */ addr.sin_family = AF_INET; addr.sin_port = htons(port); sockfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); /* Ñîçäà¸ì ñîêåò */ if(sockfd == -1) /* Ñîçäàí ëè ñîêåò */ return -1; addr.sin_addr.s_addr = INADDR_ANY; /* Ñëóøàåì íà âñåõ àäðåñàõ */ if(bind(sockfd, (sockaddr*) &addr, sizeof(addr))) /* Ïðèñâàèâàåì ñîêåòó ëîêàëüíûé àäðåñ */ return -1; if(listen(sockfd, 1)) /* Íà÷èíàåì ïðîñëóøèâàíèå */ return -1; s = accept(sockfd, (sockaddr *) &raddr, sizeof(raddr)); /* Ïðèíèìàåì ñîåäèíåíèå */ pid = fork(); /* ïîðîæäàåì äî÷åðíèé ïðîöåññ */ if(pid == 0){

передать в качестве аргумента указатель на сокет (обычно в функцию потока можно передавать данные любого типа в формате void *, что требует использования приведения типов). Важное замечание для систем Windows. Мною было замечено, что система сокетов не работает без применения функции WSAStartup для инициализации библиотеки сокетов. Программа с сокетами в ОС Windows должна начинаться так: WSADATA wsaData; WSAStartup(0x0101, &wsaData);

И при выходе из программы пропишите следующее: WSACleanup();

Так как в основном операции с сокетами являются блокирующими, приходится часто прерывать исполнение задачи ожиданием синхронизации. Поэтому часто в *nix подобных системах избегают блокировки консоли созданием особого типа программы — демона. Демон не принадлежит виртуальным консолям и возникает, когда дочерний процесс вызывает fork, а родительский процесс завершается раньше, чем 2-й дочерний (а это всегда бывает именно таким образом). После этого 2-й дочерний процесс становится основным и не блокирует консоль. Приведу пример такого поведения программы: pid = fork(); /* Ñîçäàíèå ïåðâîãî äî÷åðíåãî ïðîöåññà */ if (pid <0){ /* Îøèáêà âûçîâà fork */ printf(«Forking Error : )\n»); exit(-1); }else if (pid !=0 ){ /* Ýòî ïåðâûé ðîäèòåëü! */ printf(«\nThis is a Father 1\n»); }else{ pid = fork(); /* Ðàáîòà 1-ãî ðîäèòåëÿ çàâåðøàåòñÿ */ /* È ìû âûçûâàåì åù¸ îäèí äî÷åðíèé ïðîöåññ */ if (pid <0){

/* Ýòî äî÷åðíèé ïðîöåññ */ if(recv(s, buf, sizeof(buf), 0) < 0) /* Ïîñûëàåì óäàë¸ííîìó ñîêåòó ñòðîêó s */ return -1 if(send(s, str, sizeof(str), 0) < 0) /* Ïîëó÷àåì îòâåò îò óäàë¸ííîãî ñåðâåðà */ return -1;

}

}

printf(«Recieved string was: %s», buf); /* Âûâîä áóôåðà íà ñòàíäàðòíûé âûâîä */ close(s); /* Çàêðûâàåì ñîêåò */ return 0; /* Âûõîäèì èç äî÷åðíåãî ïðîöåññà */

/* À âîò ýòî òîò ñàìûé 2-é äî÷åðíèé ïðîöåññ */ /* Ïåðåõîä â «ñòàíäàðòíûé» ðåæèì äåìîíà */ setsid(); /* Äàííûé ïðîöåññ ñòàíîâèòñÿ ãëàâíûì â ãðóïïå */ umask(0); /* Ñòàíäàðòíàÿ ìàñêà ôàéëîâ */ chdir(«/»); /* Ïåðåõîä â êîðíåâîé êàòàëîã */ daemoncode(); /* Ñîáñòâåííî ñàì êîä äåìîíà */ /* Ïðè âûçîâå fork äåìîíà ïîÿâëÿåòñÿ ïîòîìîê-äåìîí */ }

close(sockfd); /* Çàêðûâàåì ñîêåò äëÿ ïðîñëóøèâàíèÿ */ return 0;

При создании потока (thread) для обработки сокета смотрите руководство к ОС, так как для разных систем вызов функции создания потока может существенно различаться. Но принципы обработки для потока остаются теми же. Функции обработки необходимо только

82

printf(«Forking error : )\n»); exit(-1); }else if (pid !=0 ){ /* Ýòî âòîðîé ðîäèòåëü */ printf(«\nThis is a father 2\n»); }else{

}

Вот и всё. Я думаю, для создания простенького сокетного сервера этого достаточно.


ОБРАЗОВАНИЕ


образование

ВЗАИМНЫЕ ФУНКЦИОНАЛЬНЫЕ ЗАВИСИМОСТИ

АНДРЕЙ ФИЛИППОВИЧ

84


образование Обычно к системному администратору обращаются по самым разным вопросам, при возникновении любой проблемы: будь это забытый пароль, неработающая программа, нечитающаяся дискета или пропавшие неизвестно куда данные. Этот список можно продолжать бесконечно. Мне хотелось бы остановиться на ошибках, которые возникают при работе с СУБД. Статья посвящена тем, кому приходится заниматься вопросами проектирования или реструктурирования реляционных баз данных.

Однако ни в одной из них не говорится об ограниченности их использования. Рассмотрим пример использования правила объединения: Если A→B, A→С то A→BС. Пусть имеется два простейших отношения ЗАРПЛАТА(Сотрудник, Зарплата) и СЧЕТА(Сотрудник, N_Кредитки). Во второй таблице задаются номера кредитных карточек сотрудников. Предположим, что у Невезинского нет кредитки. Специалист по БД решил оптимизировать структуру и устранить избыточность, пользуясь вышеупомянутым правилом. Если Сотрудник → Зарплата, Сотрудник → N_Кредитки то Сотрудник → {Зарплата, N_Кредитки}. ·ÔØÖÙÊÓÎÐ

В настоящее время базы данных являются частью практически любой информационной системы. Современные БД характеризуются большой размерностью и сложной структурой, поэтому для их разработки используются специальные программные средства. Они позволяют не только упростить и ускорить процесс проектирования, но и выполнять некоторые функции по оптимизации БД. Наиболее популярным является подход, в котором осуществляется нормализация. Теория нормализации (зависимостей) появилась одновременно с теорией реляционной алгебры. Коддом были предложены первые нормальные формы. Впоследствии, третья нормальная форма была уточнена и названа нормальной формой Бойса-Кодда (НФБК). Позже Фэйджином (Fagin) были предложены 4 и 5 нормальная формы, а также альтернативная доменно-ключевая нормальная форма [3]. Существуют и другие нормальные формы, но наиболее популярной является 3НФ и НФБК. В основе теории нормализации лежат различные понятия зависимостей между атрибутами БД. В теории Кодда используется понятие функциональной зависимости (ФЗ), реляционный аналог математической функции. Рассмотрение основных вопросов статьи требует четкого определения понятия ФЗ, поэтому приведем формальное определение ФЗ из 6-ого издания книги Дейта [4]. Отметим сразу, что статья ориентирована на читателя, знакомого с основными понятиями реляционной алгебры и теории нормализации. Пусть R — это отношение, а X и Y — произвольные подмножества множества атрибутов отношения R. Тогда Y функционально зависимо от X (X→Y), тогда и только тогда, когда каждое значение множества X отношения R связано в точности с одним значением множества Y отношения R. Иначе говоря, если два кортежа отношения совпадают по X [t1(X) = t2(X)], то они также совпадают и по Y [t1(Y) = t2(Y)]. X→Y o [t1(X) = t2(X)] => [t1(Y) = t2(Y)]

Возможность применения правил Армстронга Проектирование схемы базы данных начинается с определения универсального отношения, в которое входят все атрибуты. Для предметной области задается множество ограничений с помощью функциональных, многозначных и других зависимостей. Для вывода новых ФЗ или сокращения их числа используются правила Армстронга. Эти правила общеизвестны и приводятся почти в каждой книге по БД.

№1, октябрь 2002

·ÔØÖÙÊÓÎÐ

­ÆÖÕÑÆØÆ

1B°ÖËÊÎØÐÎ

³ËÈËÍÎÓ×ÐÎÏ

µÔÝØÔÈ

µÔÝØÔÈ

·ÆÖÐÎ×âåÓ

·ÆÖÐÎ×âåÓ

¸ÖÎÚÔÓÔÈ

¸ÖÎÚÔÓÔÈ

·ÔØÖÙÊÓÎÐ

­ÆÖÕÑÆØÆ

³ËÈËÍÎÓ×ÐÎÏ

1B°ÖËÊÎØÐÎ 1XOO

µÔÝØÔÈ

·ÆÖÐÎ×âåÓ

¸ÖÎÚÔÓÔÈ

Сразу после «оптимизации» в результирующем отношении появится кортеж, который имеет пустое (неопределенное) значение. Реляционное отношение, да и ФЗ (A→BС) таких кортежей не поддерживают, поэтому такая запись должна автоматически удалиться1. Большинство современных СУБД поддерживают трехзначную логику (с использованием Null значений), поэтому запись может не пропасть. Однако наличие неопределенных значений приводит к появлению неоднозначности в запросах и программах. Таким образом, свободно применять правила Армстронга можно только для проектируемой БД, не имеющей данных. При реструктуризации необходимо использовать Nullзначения. Это замечание существенно для баз данных с динамически изменяемой структурой, к которым можно отнести современные объектно-реляционные разработки. Функциональные зависимости являются одними из простейших семантических ограничений, поэтому можно смело утверждать, что все рассматриваемые вопросы будут актуальны и для объектных СУБД.

Проблемы 3НФ и НФБК Приведение отношений в 3НФ и НФБК направлено на избавление от транзитивных зависимостей (A→B, B→C => A→C). 3НФ позволяет удалить транзитивные зависимости неключевого атрибута (C) от ключевого (A) через другой неключевой (B). В НФБК запрещаются все зависимости среди ключевых атрибутов и любой неключевой атрибут должен напрямую (нетранзитивно) зависеть от ключа, т.е. от всех ключевых атрибутов. Рассмотрим пример приведения отношения в третью нормальную форму, предложенный Ульманом[18] и проблемы, которые при этом возникают. Пусть задана схема отношений R и множество функциональных зависимостей F. Звездочкой (*) помечены функци-

85


образование ональные зависимости, которые отсутствуют в указанном примере. R = (A, B, C, D, E, T), где A – читаемый курс, B – преподаватель, C – час начала занятий, D – аудитория, E – студент, T – оценка по курсу.

молетов с номером рейса из B. По условиям, заданным набором ФЗ такая ситуация невозможна. И тем не менее, в БД, находящейся в НФБК с «сохранением» зависимостей содержится неутешительный приговор двум самолетам. Рассмотрим другой случай. Пусть D — это сумма в размере 120000$ и ее нужно перечислить только на счет Иванова. Как видно из рис.1. эту сумму может также получить и Петров, а также десятки других сотрудников и это не будет противоречить структуре БД. Из примера видно, что приведение отношения в 3НФ или даже в НФБК с помощью декомпозиции не решает проблемы противоречивости хранимых данных. Конечно, если произвести естественное соединение всех отношений, то мы избавимся от всех противоречий. Обеспечение целостности по взаимосвязанности требует использования специальных средств: внешних ключей, представлений, ограничений на целостность, триггеров и т.д2.

F = {CD→B, CD→A, AC→D, CE→A, A→B, CB→D, AE→T, CE→D} A→B — каждый курс ведет один преподаватель; CD→A — в аудитории в один и тот же момент времени может читаться только один курс; CB→D — преподаватель в один и тот же момент времени может находиться только в одной аудитории; CE→D — студент в один и тот же момент времени может находиться только в одной аудитории; AE→T — по каждому курсу каждый студент имеет только одну оценку; AC→D* — каждый курс в один и тот же момент времени может читаться только в одной аудитории; CD→B*— в аудитории в один и тот же момент времени может быть один преподаватель; CE→A* — каждый студент в один и тот же момент времени может слушать только один курс.

Оставшаяся часть статьи посвящена вопросу обнаружения и устранения ошибок, описанных выше. Рассматриваемая противоречивость данных связана с тем, что во множестве ФЗ имеются взаимные функциональные зависимости (ВФЗ). Взаимной функциональной зависимостью атрибутов A и B называется пара функциональных зависимостей вида B→A, A→B и обозначается как A↔B.

Для приведения схемы отношения в 3НФ необходимо найти минимальное покрытие множества функциональных зависимостей. В описываемом примере оно будет представлено следующим образом[18]: F = {A→B, CD→A, CB→D, AE→T, CE→D} Далее, осуществим декомпозицию схемы отношения: r={AB, CDA, CBD, AET, CED}

Рассмотрим основное свойство ВФЗ. Из функциональной зависимости A→B вытекает утверждение 1. Кроме того, может существовать множество кортежей с разными значениями в атрибуте А и одинаковыми значениями в атрибуте B (утверждение 2): (1) A→B o [ti(A) = tj(A)] => [ti(B) = tj(B)] (2) [∀ ti(B)] ∃ {t(A)} , |{t(A)}|?1

Данная схема находится в НФБК и декомпозируется из исходной схемы с сохранением зависимостей. Недостатком этой декомпозиции является зависимость проекций (по терминологии Риссанена). Отсюда следует, что схема может обладать аномалиями. Проиллюстрируем это на примере: На рис.1. представлены три таблицы из полученной схемы отношений. Звездочками отмечены ключевые поля. Пусть в БД хранится информация, что в 10 часов в 12 и 13 аудиториях должны читаться курсы «Базы данных» и «Операци· ' $ онные системы» (CDA). Извест §ÆÍá ªÆÓÓáÛ но, что эти курсы читают соответ ´ÕËÖ ×Î×Ø ственно преподаватели Иванов и Петров (AB). При этом оба пре подавателя должны проводить $ % занятия одновременно в одной §ÆÍá ªÆÓÓáÛ ®ÈÆÓÔÈ аудитории (CBD). Данная инфор´ÕËÖ ×Î×Ø µËØÖÔÈ мация является противоречивой, несмотря на то, что схема отно· % ' шений находится в НФБК и все ®ÈÆÓÔÈ условия ФЗ соблюдены. µËØÖÔÈ Представим теперь, что D — Рис.1. Противоречивость БД. это номер посадочной полосы са-

Аналогично определим соотношения для ФЗ B→A: (3) B→A o [ti(B) = tj(B)] => [ti(A) = tj(A)] (4) [∀ti(A)] ∃ {t(B)} , |{t(B)}|?1

86

Взаимные функциональные зависимости

Соединяя условия (1), (4) и (2), (3) получаем следующие утверждения: (5) [∀ ti(A)] ∃ {t(B)} , |{t(B)}|=1 (6) [∀ ti(B)] ∃ {t(A)} , |{t(A)}|=1 Основным свойством ВФЗ является взаимная однозначность значений для атрибутов левой и правой части, т.е. каждый кортеж должен иметь уникальные значения полей, входящих во ВФЗ. Для задания ВФЗ в СУБД необходимо атрибуты A и B объединить в одном отношении (таблице) и задать уникальность каждого атрибута. Если части ВФЗ более одного атрибута, то можно использовать первичный и альтернативный ключ (с обязательным заданием уникальности). Надо отметить, что алгоритмы нахождения минимального покрытия Мейера [11] и Бернштейна [19] учитывают ВФЗ, но только в рамках сокращения размера покрытия. Для дальнейшего рассмотрения материала введем понятие условной ВФЗ (УВФЗ).


образование Условной взаимной функциональной зависимостью атрибутов A и B называется пара функциональных зависимостей вида СB→A, СA→B, где С — набор атрибутов (условие) такой, что CCA=?, CCB=?. Будем обозначать УВФЗ как С|A↔B. Смысл УВФЗ заключается в том, что при определенных условиях атрибуты находятся во взаимной функциональной зависимости. Из функциональной зависимости CA→B вытекают утверждения 7 и 8: (7) CA→B o [ti(C) = tj(C)]&[ti(A) = tj(A)] => [ti(B) = tj(B)] (8) [∀ ti(B)] ∃ {t(СA)} , |{t(СA)}|?1 Аналогично определим соотношения для ФЗ СB→A: (9) СB→A o [ti(C) = tj(C)]&[ti(B) = tj(B)] => [ti(A) = tj(A)] (10) [∀ ti(A)] ∃ {t(СB)} , |{t(СB)}|?1 Соединяя условия (7-10) и фиксируя значение атрибута С, получаем следующие утверждения: (11) [∀ ti(A)] ∃ {t(B)} , |{t(B)}|=1 (12) [∀ ti(B)] ∃ {t(A)} , |{t(A)}|=1 Основным свойством EВФЗ является взаимная однозначность значений для атрибутов левой и правой части при совпадении значений в атрибутах условия, т.е. каждый для каждого значения атрибутов условия кортеж должен иметь уникальные значения полей, входящих во ВФЗ. Существует несколько способов задать УВФЗ. Можно ввести понятие условного ключа или условной уникальности. Проиллюстрируем эти понятия. Пусть имеется условная взаимная зависимость (УВФЗ) такая, что условием является набор атрибутов C, атрибуты B и D входят во ВФЗ. Тогда при фиксированном значении атрибутов из С значения атрибутов B и D являются уникальными. Для пояснения приведем пример. В настоящее время возможность введения таких ключей в СУБД отсутствует, поэтому данное ограничение можно реализовать либо программным способом, либо декомпозицией отношений и дополнительных изменений схемы. К первому способу можно отнести возможность работы с представлениями (запросами на отображение). Если реализовать ввод данных только через представление, тогда оно должно содержать выборку по одному из значений условных атрибутов (например С1), а взаимозависимые атрибуты должны иметь уникальный ключ. CREATE VIEW UslFD AS SELECT Pse1.* FROM table1 AS Pse1, table1 AS Pse2 WHERE (Pse1.C= Pse2.C) & (Pse1.A<> Pse2.A) & (Pse1.B<> Pse2.B) · ½Æ× ÓÆÝÆÑÆ ÍÆÓåØÎÏ

% µÖËÕÔÊÆÈÆØËÑâ

' ¦ÙÊÎØÔÖÎå

·

% µÖËÕÔÊÆÈÆØËÑâ

·

µËØÖÔÈ

·

®ÈÆÓÔÈ

·

·ÔÐÔÑÔÈ

·

% µÖËÕÔÊÆÈÆØËÑâ

' ¦ÙÊÎØÔÖÎå

·

µËØÖÔÈ

·

·ÔÐÔÑÔÈ

№1, октябрь 2002

' ¦ÙÊÎØÔÖÎå

При наборе условных атрибутов (C1,.. Cn) конструкция Where изменится: WHERE (Pse1.C1= Pse2.C1) &(Pse1.C2= Pse2.C2) & & (Pse1.Cn= Pse2.Cn) & (Pse1.A<> Pse2.A) & (Pse1.B<> Pse2.B)

Альтернативой может послужить общее ограничения на целостность БД.

)

CREATE ASSERTION UslFD CHECK ( SELECT Pse1.* FROM table1 AS Pse1, table1 AS Pse2 WHERE (Pse1.C= Pse2.C) & (Pse1.A<> Pse2.A) & (Pse1.B<> Pse2.B)

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

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

Следствие 1 Во всех функциональных зависимостях, в которых присутствует набор условных атрибутов (С) и условная взаимная ФЗ (CA→B, CB→A), можно воспользоваться правилом A~B для нахождения минимального (канонического) покрытия. Пример: F = {CD→B, AC→D, CE→A, A→B, CD→A, CB→D, AE→T, CE→D} CD→B & CB→D => B→D & D→B => C| D↔B Выберем все зависимости, в которых присутствует C: CD→B, AC→D, CE→A, CD→A, CB→D, CE→D Заменим B на D и получим: CD→D, AC→D, CE→A, CD→A, CD→D, CE→D Сократим одинаковые зависимости (CD→D). Полученная зависимость CD→D является рекурсивной3. В исходных данных такой зависимости нет, поэтому можно проверить предметную область на ее наличие. В случае ее обнаружения необходимо разбить ее на нерекурсивные зависимости4. В общем (данном) случае рекурсии нет, поэтому эту функциональную зависимость исключаем из списка как тривиальную. AC→D, CE→A, CD→A, CE→D, C| D↔B Аналогично поступаем и AC→D и CD→A CE→A, C| A↔D↔B Итоговое множество функциональных зависимостей будет выглядеть следующим образом: F = { A→B, AE→T, CE→A, C| A↔D↔B}

87


образование Следствие 2 Более простые УВФЗ включают в себя более сложные. Если имеется две УВФЗ такие, что множество условных атрибутов одной зависимости входит во множество условных атрибутов другой зависимости, то такая УВФЗ является более простой: C|A↔B и D|A↔B, и CID, то C|A↔B => D|A↔B

Следствие 3 Нахождение минимального покрытия ФЗ не убирает и не изменяет ВФЗ, но значительно осложняет их поиск. В качестве примера можно сравнить очевидность ВФЗ в исходном и нормализированном наборе ФЗ (см. выше). Одним из способов обнаружения ВФЗ является графическое отображение ФЗ и поиск кольцевых структур5. CA→B, B→A На рис.2а показаны функциональные зависимости в графической форме. Можно заметить, что функциональные зависимости пересекаются, и, кроме того, образуют циклическую структуру. Воспользовавшись правилом дополнения Армстронга, получаем схему, представленную на рис.2б. Функциональная зависимость B→A представлена отдельно и атрибут B в ней не зависит от CA, т.к. зависимость была разделена на B→A при наличии C и на B→A при отсутствии C. На рис.2в представлено выделение взаимной зависимости A и B при условии С. На рисунке 3 рассматривается пример нахождения УВФЗ для минимального покрытия ФЗ из примера, приведенного выше. В результате нахождения минимального покрытия ВФЗ становятся менее очевидными из-за увеличения элементов в цикле. Взаимные зависимости становятся транзитивными. Можно сделать вывод, что нахождение минимального покрытия ФЗ усложняет процесс нахождения ВФЗ за счет уменьшения числа зависимостей. Тем не менее, нахождение минимального покрытия необходимо для по-

à

á Рисунок 2. $

â

%

& %

'

'

&

$

%

Рисунок 3.

88

&

$

'

'

$ (

& %

$

7

& (

'

лучения 3НФ, а некоторые ВФЗ могут быть исходно неочевидны и для их выявления требуется ввести специальный алгоритм. Единственное, что можно посоветовать, это проверка на ВФЗ каждой ФЗ, которую можно исключить из начального списка ФЗ. Ниже приводится алгоритм нахождения ВФЗ для минимального покрытия. Данный алгоритм является схематичным и неоптимизированным, т.к. при нахождении ВФЗ не осуществляется изменение (уменьшение) множества исходных ФЗ. Можно также заранее исключить зависимости, которые точно не образуют ВФЗ. Это так называемые неприводимые слева ФЗ, т.е. те зависимости, в левой части которых содержатся атрибуты, не встречающиеся в правой части других ФЗ. Алгоритм можно также применять для произвольного множества ФЗ, но в случае минимального покрытия ВФЗ будут находится только один раз. Существуют множества ФЗ, которые являются взаимно-независимые. Правила определения таких множеств можно найти в [4,20]. Пусть задана схема отношений R и множество ФЗ S такие что: S ={ X[1]→Y[1]… X[i]→Y[i]… X[n]→Y[n]}, где n — количество ФЗ. Y[i]∈ R, X[i]⊆ R , т.е. Y является атрибутом, а X — набором атрибутов из R. X[i]= {X[i][1]… X[i][j]… X[i][m]}, где m — Количество атрибутов в левой части ФЗ. Для разных ФЗ m может изменяться.

Алгоритм for i:=1 to n do { m:=m(X[i]);

// öèêë ïî âñåì ÔÇ // ïîäñ÷åò êîëè÷åñòâà àòðèáóòîâ â ëåâîé ÷àñòè for j:=1 to m do // öèêë ïî âñåì àòðèáóòàì {Left_Part:= X[i][j]; // âûáîð îäíîãî èç àòðèáóòîâ for k:=1 to n do // öèêë ïî âñåì ÔÇ // åñëè óñëîâèå âûïîëíÿåòñÿ, òî âîçìîæíî íàëè÷èå êîëüöåâîé ñòðóêòóðû {if Left_Part == Y[k] then //ôóíêöèÿ ïðîâåðÿåò ìíîæåñòâî S íà íàëè÷èå ÂÔÇ //ìåæäó X[i][j] è Y[i] ïðè óñëîâèè X[i]- X[i][j] call function_1(Left_Part, Y[i], X[i]- X[i][j]); Restore S, n; //âîññòàíîâëåíèå ïîëíîãî ñïèñêà ÔÇ } } } //ôóíêöèÿ ïðîâåðÿåò ìíîæåñòâî S íà íàëè÷èå ÂÔÇ //ìåæäó Left_Part è Right_Part ïðè óñëîâèè Usl function_1(Left_Part, Right_Part, Usl) for i:=1 to n do // öèêë ïî âñåì ÔÇ { if (Right_Part I X[i]) then //åñëè ïðàâûé àòðèáóò âõî äèò // â ëåâóþ ÷àñòü êàêîé-ëèáî ÔÇ { Usl:=Usl+ X[i] - Right_Part; // òî óñëîâèå äîïîë íÿåòñÿ äðóãèìè //àòðèáóòàìè èç ëå âîé ÷àñòè if (Left_Part == Y[i]) then //åñëè ïðîèçîøëî çàêîëüöåâàíèå { Add «Usl | Left_Part ~ Right_Part» //äîáàâëÿåò ñÿ íîâàÿ ÓÂÔÇ delete X[i]®Y[i]; / / äàííàÿ ÔÇ óäàëÿåòñÿ èç S n:=n-1; //êîëè÷åñòâî ÔÇ óìåíüøàåòñÿ } else //èíà÷å îñóùåñòâëÿåòñÿ ðåêóðñèâíûé âûçîâ äëÿ äàëüíåéøåãî ïîèñêà


образование { Right_Part := Y[i]; delete X[i]®Y[i]; //äàííàÿ ÔÇ óäàëÿåòñÿ èç S n:=n-1; //êîëè÷åñòâî ÔÇ óìåíüøàåòñÿ function_1(Left_Part, Right_Part, Usl); } }

}

Выводы 1. Нахождение ВФЗ позволяет избавиться от противоречивости хранимой информации в БД. 2. Одним из способов учета ВФЗ является наложение ограничений на целостность БД. Для этого введем понятие «целостности по взаимозависимости». 3. Вторым способом учета ВФЗ является нормализация отношений, т.е. приведение БД к соответствующей структуре (схеме). Введем понятие взаимно-независимой нормальной формы, если схема отношений не имеет ВФЗ. ВННФ можно рассматривать как синоним понятия ациклической БД. 4. Приведение отношения к ВННФ можно осуществлять независимо от приведения отношения в 1НФ, 2НФ, 3НФ, НФБК. Рекомендуется выявлять ВФЗ на этапе нахождения минимального покрытия. 5. В последующих нормальных формах используются несколько другие зависимости. В данной работе эти вопросы не проработаны. 6. Автор не считает, что вопросы, затронутые в этой работе являются научной новизной. Более того, автор уверен, что за такой продолжительный срок существования теории баз данных и теории нормализации, проблемы ВФЗ были затронуты, а может, и решены. К сожалению, из всех современных книг по БД, только в книге Дейта и Мейера отдаленно затрагивается этот вопрос, несмотря на его первостепенную важность. Автор осуществлял поиск и просмотрел большое количество литературы. На поиск аналогичных разработок было потрачено время, в 7-8 раз превышающее теоретическую разработку проблемы ВФЗ. «Если теорему проще доказать, чем найти описание доказательства, то почему это не сделать?» Автор пытается акцентировать внимание на вопросах ВФЗ. Если читатель имеет какую-либо информацию по данному вопросу, а также возражения, замечания или дополнения, присылайте их по адресу fil@ics.bmstu.ru. 1

Например, если использовать SQL-инструкцию Select Сотрудник, Зарплата, N_Кредитки From ЗАРПЛАТА AS R1, СЧЕТА AS R2 Where R1. Сотрудник = R2. Сотрудник

2

Попытка создать описанную БД с заданием связей в среде ERWin приведет к неудаче. 3

Рекурсивные зависимости невозможны в реляционной алгебре, называются тривиальными и исключаются из множества ФЗ как избыточные. Следует различать рекурсивные и взаимные зависимости.

№1, октябрь 2002

4

Программа ERWIN позволяет использовать на стадии проектирования рекурсивные неидентифицирующие связи. 5

Здесь очень хорошо бы подошло слово замыкание или рекурсивная связь, но они уже используются и несут несколько другую смысловую нагрузку. См. также циклические БД. Ëèòåðàòóðà: 1. Àðñåíüåâ Á.Ï., ßêîâëåâ Ñ.À. Èíòåãðàöèÿ ðàñïðåäåëåííûõ ÁÄ, ÑÏá, Ëàíü, 2001, - 464 ñ. Òåîðèÿ íîðìàëèçàöèè (ñòð. 37-41). 2. Âåðáîâåöêèé À.À. Îñíîâû ïðîåêòèðîâàíèÿ áàç äàííûõ., Ðàäèî è ñâÿçü, 2000, - 88 ñ. Òåîðèÿ íîðìàëèçàöèè (ñòð. 25-28). 3. Ãîëîñîâ À.Î. Àíîìàëèè â ðåëÿöèîííûõ áàçàõ äàííûõ. Æóðíàë ÑÓÁÄ, âûïóñê 1.06.1996. 4. Äåéò Ê.Äæ. Ââåäåíèå â ñèñòåìû áàç äàííûõ, 6-å èçäàíèå. Ê., Ì., ÑÏá.: Èçäàòåëüñêèé äîì «Âèëüÿìñ», 2000. - 848 ñ. Òåîðèÿ íîðìàëèçàöèè (ñòð. 269-347). Îäíà èç íàèáîëåå ïîëíûõ êíèã íà ðóññêîì ÿçûêå. Çàòðàãèâàþòñÿ âîïðîñû àòîìàðíûõ îòíîøåíèé è ÄÊÍÔ.  7-îì èçäàíèè áîëåå ïîäðîáíî ðàññìàòðèâàþòñÿ âîïðîñû 4 è 5ÍÔ. 5. Äóíàåâ Ñ. Äîñòóï ê ÁÄ è òåõíèêà ðàáîòû â ñåòè. Ì., Äèàëîã-ÌÈÔÈ, 2000, - 416 ñ. 6. Êàëèíè÷åíêî Ë.À. Ìåòîäû è ñðåäñòâà èíòåãðàöèè íåîäíîðîäíûõ áàç äàííûõ. Ì.: Íàóêà, 1983 - 424 ñ. 7. Êàðïîâà Ò. Áàçû äàííûõ, ìîäåëè, ðàçðàáîòêà, ðåàëèçàöèÿ, ÑÏá., Ïèòåð, 2001, - 304 ñ. Òåîðèÿ íîðìàëèçàöèè (ñòð. 110-120). 1-5ÍÔ, î÷åíü êðàòêî. 8. Êîííîëëè Ò., Áåãã Ê., Ñòðà÷àí À. Áàçû äàííûõ. Ïðîåêòèðîâàíèå, ðåàëèçàöèÿ è ñîïðîâîæäåíèå. Òåîðèÿ è ïðàêòèêà, 2-å èçä., Ì.:»Âèëüÿìñ», 2000, - 1120 ñ. Òåîðèÿ íîðìàëèçàöèè (ñòð. 222258). 1-5ÍÔ, î÷åíü êðàòêî, â îñíîâíîì, ïðèìåðû. 9. Êîðíååâ Â.Â., Ãàðååâ À.Ô., Âàñþòèí Ñ.Â., Ðàéõ Â.Â. Áàçû äàííûõ. Èíòåëëåêòóàëüíàÿ îáðàáîòêà èíôîðìàöèè. Ì.: «Íîëèäæ». 352 ñ. 10. Êóëüáà Â.Â., Êîâàëåâñêèé Ñ.Ñ., Êàÿ÷åíêî Ñ.À., Ñèðîòþê Â.Î. Òåîðåòè÷åñêèå îñíîâû ïðîåêòèðîâàíèÿ îïòèìàëüíûõ ñòðóêòóð ðàñïðåäåëåííûõ ÁÄ., Ì., Ñèíòåã, 1999, 660 ñ. Òåîðèÿ íîðìàëèçàöèè (ñòð. 116-124). 1-3ÍÔ, Ôîðìàëüíîå îïèñàíèå îñóùåñòâëåíî â òåðìèíàõ êíèãè. 11. Ìåéåð Ä. Òåîðèÿ ðåëÿöèîííûõ áàç äàííûõ. Ì.: Ìèð, 1987. 608 ñ. Òåîðèÿ íîðìàëèçàöèè ðàñêðûâàåòñÿ â ýòîé êíèãå íàèáîëåå ïîëíî. Î÷åíü ìíîãî ðåçóëüòàòîâ, ñâÿçàííûõ ñ ìíîãîçíà÷íûìè è ñîåäèíèòåëüíûìè çàâèñèìîñòÿìè. Ðàññìàòðèâàþòñÿ êîëüöåâûå è òàáëè÷íûå çàâèñèìîñòè, àëãîðèòìû ñèíòåçà, äåêîìïîçèöèè è ìíîãèå äðóãèå âîïðîñû. Êíèãà äîñòàòî÷íà ñëîæíàÿ, ò.ê. ââîäèòñÿ ìíîæåñòâî äîïîëíèòåëüíûõ ïîíÿòèé, è ïåðåîïðåäåëÿþòñÿ íåêîòîðûå ïðèâû÷íûå îïðåäåëåíèÿ. 12. Ìàðòèí Äæ. Îðãàíèçàöèÿ áàç äàííûõ â âû÷èñëèòåëüíûõ ñèñòåìàõ. 2-å èçä., Ì.: Ìèð, 1980. 13. Ðåâóíêîâ Ã.È., ×åòâåðèêîâ Â.Í., Ñàìîõâàëîâ Ý.Í. Áàçû è áàíêè äàííûõ. Ì.: Âûñøàÿ øêîëà, 1987. - 248 ñ. 14. Ôðîëîâ. À., Ôðîëîâ. Ã. Áàçû äàííûõ â Èíòåðåíåòå: ïðàêòè÷åñêîå ðóêîâîäñòâî ïî ñîçäàíèþ WEB-ïðèëîæåíèé ñ ÁÄ., èçä. 2å., «Ðóññêàÿ Ðåäàêöèÿ», 2000, - 448 ñ. 15. Õàíñåí Ã., Õàíñåí Äæ. Áàçû äàííûõ. Ðàçðàáîòêà è óïðàâëåíèå, Ì., Áèíîì, 2000, - 704 ñ. Òåîðèÿ íîðìàëèçàöèè (ñòð. 200210). 1-4ÍÔ, î÷åíü êðàòêî. 16. Öàëåíêî Ì.Ø. Ìîäåëèðîâàíèå ñåìàíòèêè â ÁÄ. Ì.: Íàóêà. Ãë. ðåä. ôèç-ìàò.ëèò., 1989. - 288 ñ. - (Ïðîáëåìû èñêóññòâåííîãî èíòåëëåêòà). 17. Óëüìàí Äæ., Óèäîì Äæ. Ââåäåíèå â ñèñòåìû ÁÄ. Ì.: Ëîðè, 2000, - 420 ñ. Òåîðèÿ íîðìàëèçàöèè (ñòð. 94-138). 1-5ÍÔ, Ïåðåèçäàíèå êíèãè 80 ãîäà ñ íåáîëüøèìè èçìåíåíèÿìè. Ïðèâîäÿòñÿ àëãîðèòìû ïðèâåäåíèÿ â ÍÔ. 18. Óëüìàí Äæ. Îñíîâû ñèñòåì áàç äàííûõ. - Ì.: Ôèíàíñû è ñòàòèñòèêà, 1983. - 334 ñ. Òåîðèÿ íîðìàëèçàöèè (ñòð. 152-189). 1-4ÍÔ, Îäíà èç íåìíîãèõ êíèã, ñîäåðæàùàÿ ìíîæåñòâî àëãîðèòìîâ, òåîðåì, àêñèîì äëÿ ôóíêöèîíàëüíûõ è ìíîãîçíà÷íûõ çàâèñèìîñòåé. 19. Bernstein P.A. Synthesising Third Normal Form Relations from Functional Dependencies // ACM Trans. on Database Systems. - 1976. V. 1, ¹ 4. - Ð. 277-298. 20. Rissanen J. Independent Components of Relations// ACM Trans. on Database Systems. - 1977. - V. 2, ¹4. - Ð. 317-325. 21.Zaniolo C., Melkanoff M.A. A Formal Approach to the Definition and the Design of Conceptual Schemata for Database Systems // ACM Trans. on Database Systems. - 1982. - V. 7, ¹.1, - P. 24-59.

89


CommerceML

стандарт стандарт обмена обмена коммерческой коммерческой информацией информацией вв формате формате XML XML

РТИЩЕВА ЕЛЕНА ВАЛЕРЬЕВНА

Для обмена информацией в электронной коммерции необходим общий язык, с помощью которого компании могли бы обмениваться структурированными данными между своими разнотипными компьютерами. Язык Internet первого поколения, HTML, не подходит для этой цели - он описывает форматирование информации, но не ее смысл. И вот появился XML - Extensible Markup Language (расширяемый язык разметки). Как и HTML, он содержит текст, размеченный тегами. Но теги в XML описывают уже и смысл и структуру информации, позволяя напрямую обрабатывать ее программными средствами. Например, Международный совет по прессе и телекоммуникациям недавно утвердил NewsML как основную систему разметки новостной информации, также был создан MathML для математических документов и др.

90

Однако для конкретного бизнес-приложения сам по себе XML еще не от – вет – он лишь основа, на которой этот ответ можно построить. В соответствии с планами, объявленными в июле 2000 года, специалистами фирм «1С» и «Extra.RU» при поддержке технических специалистов представительства Microsoft в России разработаны стандарты обмена коммерческой информацией в формате XML для торговых организаций. Между компанией Microsoft®, фирмой «1С» и ведущими отечественными Интернет-компаниями «Port.ru», «Price.Ru» и «Extra.RU», а также московским представительством компании Intel достигнуто соглашение о поддержке единого стандарта обмена коммерческой информацией в формате XML. Соглашение о дальнейшем развитии и поддержке единого стандарта

предполагает совместную работу специалистов фирм «1С», «Port.ru», «Price.Ru», «Extra.RU» и компании Microsoft над совершенствованием стандарта, а также предоставление всей необходимой информации организациям, которые в дальнейшем захотят поддерживать предлагаемый стандарт. Соглашение имеет некоммерческий характер, ставит целью развитие Интернет-технологий и технологий обмена коммерческой информацией и является открытым для всех заинтересованных организаций, готовых к конструктивному сотрудничеству.

Использование CommerceML Сегодня рынок Интернет-коммерции в нашей стране находится на этапе становления. Пока что Интернетторговлей занимаются в основном


образование компании, созданные непосредственно для продажи услуг через Сеть, а также фирмы, работающие в сфере информационных технологий. Естественным путем, позволяющим вовлечь в Интернет–коммерцию широкий круг традиционных (off-line) торговых фирм, расширить сферу применения Интернет–технологий и продемонстрировать преимущества этих технологий продавцам и покупателям, является публикация каталогов фирм на специализированных Интернет-сайтах (Web-витринах). Поэтому количество Web-витрин растет, и Интернеткомпании заинтересованы в привлечении как можно большего числа торговых организаций. Однако этот процесс сдерживается отсутствием стандартов и готовых программно-аппаратных решений, а также большой трудоемкостью организации взаимодействия торговых организаций с Web-витринами.

Общее описание стандарта Разработанный стандарт позволяет существенно снизить затраты на организацию информационного взаимодействия за счет унификации обмена коммерческой информацией между различными организациями: как выступающими на рынке Интернет-коммерции, так и работающими в сфере традиционной (off-line) торговли. Использование торговыми организациями программного обеспечения, поддерживающего данный стандарт, позволит им с минимальными усилиями и без привлечения программистов организовать публикацию своих предложений на любых поддерживающих этот стандарт Webвитринах, а также реализовать обмен информацией между собой без специальной доработки программ. Например, при оприходовании товаров у покупателя информация о хозяйственной операции может быть автоматически загружена из данных, полученных от продавца. Разработчики стремились обеспечить максимальную открытость стандарта с тем, чтобы он в дальнейшем мог развиваться на основании объективных потребностей рынка и поддерживаться как можно более

№1, октябрь 2002

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

Описание схемы CommerceML Предусматривается использование данной схемы, в частности, для обмена: n каталогами товаров; n коммерческими предложениями; n документами.

Формирование коммерческих предложений по каталогу Предложение практически совпадает с одной строкой «обычного» прайс-листа. Предлагается такой-то товар по такой-то цене, имеющийся в наличии в таком-то количестве. Например, гречневая крупа по цене 200 рублей за мешок, на складе имеется 125 мешков. Предложения группируются в Пакет предложений, в котором задается общая часть всех предложений (аналог «шапки» прайс-листа). Для того чтобы получатели предложений могли понять, какой товар предлагается, последний должен быть описан. Описание товара и его классификация «складываются» в Ката-

лог. Каталог может быть «внутренним», т.е. вложенным в тот же документ, что и пакет предложений, и составленным непосредственно автором пакета предложений. Он также может быть «внешним» – составленным одной из известных фирм. В этом случае в пакете предложений оговаривается, на какой каталог (классификатор) он ориентирован. Для однозначного определения товара в последнем случае достаточно ссылки (идентификатора товара во внешнем каталоге), т.е. в тот же документ, что и пакет предложений, каталог товаров можно вообще не включать. Таким образом, каталог товаров можно рассматривать как некий классификатор. Следовательно, в каталоге должен быть оговорен список Свойств (по каким критериям производится классификация). Устойчивые сочетания свойств удобно фиксировать в Наборы свойств (например, «свойства видеомагнитофона», «свойства телевизора»). Для указания, какие свойства (или наборы свойств) доступны (могут быть определены, обязательно должны быть указаны) для всего каталога, для его группы или для отдельного товара, используются Ссылки на свойства (Ссылки на наборы Свойств). Каталог (классификатор) обычно создается многоуровневым (т.е. имеющим разветвленное дерево категорий (Групп), к которым можно отнести товар). Иногда однозначная классификация может вызвать затруднения, поэтому для удобства разрешается включать товары сразу в несколько категорий. Но при этом одна из них должна быть выбрана в качестве «основной». Например, радио-будильник можно отнести как к категории «Радиоприемники», так и к категории «Будильники», но в первую очередь, радио-будильник является радиоприемником. При разработке классификаторов принято для каждой позиции указывать Аналоги (например, для лекарства это – другие лекарства аналогичного действия, для запчастей – запчасти, которые можно поставить вместо данной). Указание, какими собственно свойствами из заданных в каталоге может обладать товар (или группа), достигается с помощью Ссылки на свойство (при этом еще можно задать

91


образование обязательность заполнения данного свойства). Аналогичный тип элемента создан и для набора (Ссылка на набор свойств). Для хранения значений свойств, в том числе и дополнительной, не предусмотренной классификатором информации, служит специальный тип элемента ЗначениеСвойства. В итоге, для опубликования своего прайс-листа (составления своего пакета предложений) надо сделать следующее.

1. Классифицировать свой товар Это можно сделать:

n путем составления собственного классификатора, для чего нужно: 1. составить список свойств, по которым будет производится классификация; 2. объединить устойчивые сочетания свойств в наборы свойств; 3. составить иерархический список категорий (групп); 4. отнести каждый товар к одной или нескольким категориям; 5. определить для каждого товара его аналоги. n путем нахождения своих товаров во внешнем классификаторе. 1. если некоторые товары не найдены во внешнем классификаторе, то для них (и только для них!) придется составлять внутренний классификатор.

2. Отправить пакет предложений 1. Если при составлении пакета предложений оказалось достаточно внешнего классификатора, то отправленный файл будет содержать только пакет предложений. 2. Если для составления пакета (всего или его части) понадобился внутренний классификатор, то в отправляемый файл придется включить внутренний классификатор.

Обмен документами В задачу, решаемую с помощью данной схемы, не входит обмен произвольными документами. Также не входят задачи поддержки распределенной базы данных. Схема описывает документы, сопровождающие наиболее распространенные торговые (хозяйственные) операции:

92

n Заказ товара n Cчет на оплату n Отпуск товара n Счет-фактура n Возврат товара n Передача товара на реализацию n Возврат товара с реализации n Отчет о продажах комиссионного товара

n Выплата наличных денег n Возврат наличных денег n Выплата безналичных денег n Возврат безналичных денег Причем для предприятий (фирм) – отправителя и получателя XML-документа – указанные хозяйственные операции представляются разными документами. Например «Отпуск товара» для отправителя сопровождается оформлением «расходной накладной» («накладной на отпуск товара»), а для получателя – оформлением «приходной накладной». Программа автоматизации учета может, исходя из вида хозяйственной операции и роли, которая указана для данного предприятия, «понять», является ли «собственное предприятие» (от лица которого автоматизируется учет в программе) получателем данного документа. Роли предусмотрены следующие: n Продавец n Покупатель n Плательщик n Получатель Например, если в обрабатываемом

XML-документе, описывающем «Отпуск товара» роль «собственного предприятия» обозначена как «Покупатель», то это означает, что XML-документ описывает расходную накладную поставщика, и ее следует импортировать в учетную систему как «накладную на поступление товара». Примеры соглашений при использовании данной схемы.

Обозначения «0-1» - атрибут или элемент не обязателен. Может принимать только одно значение; «1-1» - атрибут или элемент обязателен. Может принимать только одно значение; «0-*» - атрибут или элемент не обязателен. Может содержать список значений; «1-*» - атрибут или элемент обязателен. Может содержать список значений. По умолчанию – все атрибуты и элементы являются не обязательными и имеют тип «строка», если специально не оговорено другое.

Коммерческая Информация (CommerceInfo) Описание: Корневой элемент XMLдокумента, описывающего каталог (каталоги) товаров, список (списки) предложений. Содержит один или несколь-

Òàáëèöà ¹1 ³ÆÎÒËÓÔÈÆÓÎË °ÔÒÒËÓØÆÖÎÏ &RPPHQW

¸ÎÕ ´ÕÎ×ÆÓÎË µÖËÊÓÆÍÓÆÝËÓ ÊÑå ÕËÖËÊÆÝÎ ×ÔÕÖÔÈÔÊÎØËÑâÓÔÏ ÍÆÕÎ×ÐÎ È ÈÎÊË ÕÖÔÎÍÈÔÑâÓÔÏ ØËÐ×ØÔÈÔÏ ÎÓÚÔÖÒÆÜÎÎ ÕÔ ÊÔÐÙÒËÓØÙ

°ÔÒÒËÓØÆÖÎÏ

Òàáëèöà ¹2 ³ÆÎÒËÓÔÈÆÓÎË ³ÔÒËÖ·ÝËØÆ $FFRXQW1XPEHU §ÆÓÐ %DQN §ÆÓаÔÖÖË×ÕÔÓÊËÓØ &RUUHVSRQGHQW%DQN °ÔÒÒËÓØÆÖÎÏ &RPPHQW

¸ÎÕ ´ÕÎ×ÆÓÎË ³ÔÒËÖ ×ÝËØÆ

°ÔÒÒËÓØÆÖÎÏ

,GUHI §ÆÓÐ È ÐÔØÔÖÔÒ ÔØÐÖáØ ×ÝËØ ,GUHI §ÆÓÐ ÐÔÖÖË×ÕÔÓÊËÓØ È ×ÑÙÝÆË ÓË ÕÖåÒáÛ ÖÆ×ÝËØÔÈ µÖÔÎÍÈÔÑâÓáÏ ×ÔÕÖÔÈÔÊÎØËÑâÓáÏ ØËÐ×Ø

Òàáëèöà ¹3 ³ÆÎÒËÓÔÈÆÓÎË ®ÊËÓØÎÚÎÐÆØÔÖ ,G ³ÆÎÒËÓÔÈÆÓÎË 1DPH °ÔÒÒËÓØÆÖÎÏ &RPPHQW

¸ÎÕ ´ÕÎ×ÆÓÎË ,G ¹ÓÎÐÆÑËÓ È ÖÆÒÐÆÛ È×ËÉÔ ÊÔÐÙÒËÓØÆ ³ÆÎÒËÓÔÈÆÓÎË ÓÆÕÖÎÒËÖ ªÔÒÆÞÓÎÏ ØËÑËÚÔÓ ÊÎÖËÐØÔÖÆ µÖÔÎÍÈÔÑâÓáÏ ×ÔÕÖÔÈÔÊÎØËÑâÓáÏ ØËÐ×Ø ÓÆÕÖÎÒËÖ ÐÆÐ ÊÔËÛÆØâ ÓÆ ÆÈØÔÒÔÇÎÑË

°ÔÒÒËÓØÆÖÎÏ


образование ко каталогов товаров, включая список возможных свойств товаров в каталоге, один или несколько пакетов предложений. Атрибуты: Таблица №1 Содержит: Каталог (0-*), ПакетПредложений (0-*), Контрагент (0-*), Документ (0-*), Банк (0-*), Склад (0-*).

РасчетныйСчет (BankAccount) Описание: Расчетный счет описывает банковский счет контрагента в объеме, необходимом для оформления (и передачи) документов. Атрибуты: Таблица №2 Содержит: ДополнительныйРеквизит (0-*).

Контакт (Contact) Описание: Контакт предназначен для ответа на вопросы: «Где найти?» и «Кого спросить?». Содержит список ФИО контактных лиц (например, список сотрудников отдела продаж), список телефонов, факсов, адресов электронной почты контакта, ICQ. Атрибуты: Таблица №3 Содержит: КонтактноеЛицо (0-*), Телефон (0-*), Факс (0-*), Почта (0*), ICQ (0-*).

Пример структуры XMLсхемы в формате HTML. <КоммерческаяИнформация> собирательный элемент для всего, что мо-

°ÔÒÒËÖÝË×ÐÆå®ÓÚÔÖÒÆÜÎå > °ÔÒÒËÓØÆÖÎÏ VWULQJ @ ! °ÔÓØÖÆÉËÓØ ! @ > ·ÐÑÆÊ ! @ ×ÎÓØÆÐ×Î× > §ÆÓÐ ! @ VHT > °ÆØÆÑÔÉ ! @ > µÆÐËصÖËÊÑÔÌËÓÎÏ ! @ > ªÔÐÙÒËÓØ ! @ °ÔÒÒËÖÝË×ÐÆå®ÓÚÔÖÒÆÜÎå ! ×Ô×ØÆÈ HOW2QO\ ÕÔÖåÊÔÐ VHT ÈáÞË×ØÔåßÎË 1R SDUHQWV IRXQG 7KLV LV SUREDEO\ WKH GRFXPHQW HOHPHQW ÊÔÝËÖÓÎË §ÆÓÐ ªÔÐÙÒËÓØ °ÆØÆÑÔÉ °ÔÓØÖÆÉËÓØ µÆÐËصÖËÊÑÔÌËÓÎÏ ·ÐÑÆÊ ÆØÖÎÇÙØá °ÔÒÒËÓØÆÖÎÏ ÒÔÊËÑâ RSHQ ÕÔ ÙÒÔÑÝÆÓÎä <ElementType name=»Êîììåð÷åñêàÿÈíôîðìàöèÿ» content=»eltOnly» order=»seq»> <description> Ñîáèðàòåëüíûé ýëåìåíò äëÿ âñåãî, ÷òî ìîæåò áûòü óïîìÿíóòî â ïðîöåññå îáìåíà</description> <AttributeType name=»Êîììåíòàðèé» dt:type=»string» required=»no»> description>Ïðåäíàçíà÷åí äëÿ ïåðåäà÷è «ñîïðîâîäèòåëüíîé çàïèñêè» â âèäå ïðîèçâîëüíîé òåêñòîâîé èíôîðìàöèè ïî äîêóìåíòó.</description> </AttributeType> <attribute type=»Êîììåíòàðèé»/> <element type=»Êîíòðàãåíò» minOccurs=»0" maxOccurs=»*»/> <element type=»Ñêëàä» minOccurs=»0" maxOccurs=»*»/> <element type=»Áàíê» minOccurs=»0" maxOccurs=»*»/> <element type=»Êàòàëîã» minOccurs=»0" maxOccurs=»*»/> <element type=»ÏàêåòÏðåäëîæåíèé» minOccurs=»0" maxOccurs=»*»/> <element type=»Äîêóìåíò» minOccurs=»0" maxOccurs=»*»/> </ElementType>

жет быть упомянуто в процессе обмена. <description> - предназначен для передачи «сопроводительной записки» в виде произвольной текстовой информации по документу. Представленный стандарт обмена коммерческой информацией дает возможность организациям обмениваться информацией в одном форма-

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


женщина и компьютер

Уступите место женщине! С древних времен отношения между полами складывались на основании физического превосходства. Такие отношения остались и сегодня. До сих пор «положено», что мужчина – сильный пол – занимается тяжелой работой и добыванием еды, а женщина – слабое создание – должна рожать детей и держать в порядке дом. Мужчина – сильный, а женщина – мудрая. Только сегодня все немного перепуталось: для добывания еды мужчине не надо больше ходить на охоту, а достаточно весь день просидеть у экрана монитора, нажимая кнопочки. И чем эта работа такая тяжелая и трудоемкая? Сегодня такие профессии, как системный администратор или программист приобрели метку сугубо мужского способа зарабатывания кучи денег. Сегодня эти специальности очень престижны и высокооплачиваемы в России (не совсем так, глав.ред.), а тем более за рубежом. А почему бы женщине не заняться такой простой работой – и мудрость можно применить, и физической нагрузки практически никакой. Так почему же женщине так тяжело найти работу в этих отраслях? Почему даму неохотно берут на работу даже тестером, не говоря уже о программистах? Столкнувшись с такой дискриминацией по половому признаку, мы с подругой решили провести небольшое исследование в этой области (а заодно и устроиться на работу), прибегнув к помощи приятельницы, которая работает в агентстве по трудоустройству. Резюме выглядели следующим образом: я - студентка шестого курса МГТУ им. Баумана, знания СУБД: Access, MS SQL 7.0, администрирование 1С-Предприятие 7.7; базовые знания бухучета, опыт Web-разработок, знание английского языка. Марина – в этом году закончила МГТУ с красным дипломом, имела разовый опыт разработки программы для небольшой фирмы, знания: Delphi, MS SQL 7.0, базовые знания 1С, технический английский + разговорный, начала изучение C++.

94

Светлана через свое агентство нашла нам несколько серьезных работодателей и оформила нас на собеседования. Вакансии были выбраны такие: системный администратор, тестировщик, программист. Мы ожидали услышать байки про глупеньких девушек, которые готовы упасть в обморок, когда их просят дать «подмышку», долго ищут ту кнопку, которая все выключает, или путает монитор с телевизором. Однако ответы были совершенно неожиданными. Первый ответ был больше похож на отмазку: в объявлении говорилось, что фирма не против взять студента старших курсов технического ВУЗа с базовыми знаниями MS SQL 7.0/2000. В фирме же мне сказали, что брать студентку они не намерены, при этом не объясняя конкретной причины. С Мариной же была проведена беседа о «личном», где подробно допытывались информации о ее семейном положении. В итоге ответ был весьма суров: «Девушку на работу не возьму… Девушка становится мамой, а программирование – это…», и далее - длинный монолог о непрерывности и качественности интеллектуального процесса. В другом, исключительно мужском коллективе, честно признались, что если в отделе будет постоянно находиться женщина (особенно молодая), то работать будет тяжелее: придется каждый день бриться, следить за чистотой рубашек, контролировать свою речь и вообще соответствовать «стандарту». Вывод был однозначен – присутствие барышни, безусловно, нарушит гармонию в этом коллективе, и нас здесь не ждут… А Светлана со своей стороны узнала от руководителя отдела, автора многочисленных научных публикаций с педагогическим образованием, что русского литературного языка не хватает для постановки задачи, поэтому он не сможет корректно поставить задачу женщине-программисту, т.е. без нормативной лексики…

Для себя с Мариной мы отметили следующее: в этих фирмах никто не поинтересовался нашим уровнем профессионализма. Еще скажу пару слов о размерах заработной платы: у женщин оплата труда, в основном, все-таки ниже, чем у мужчин. По данным все того же кадрового агентства, средний уровень заработной платы для мужчины-программиста Oracle составляет $600 - $800 в месяц (в отдельных местах, до $1200), а женщинам платят всего $200 - $400. Причем, малейшая профессиональная ошибка, допущенная женщиной-программистом, рассматривается коллегами с пристальным вниманием. Наша собственная статистика такова: из 16 фирм, в которые были направлены наши резюме, только восемь не остались без ответа (учитывая, что кадровое агентство уже проводило первичное собеседование), а из восьми оказалось 6 отказов. Так что же теперь? Неужели нам, женщинам, так и не суждено найти работу по специальности? Напротив! Женщина своего добьется! В различного рода компаниях, занимающихся разработками бухгалтерского программного обеспечения, в торговых центрах, в банках, да и во многих других местах успешно работают женщины: WEB-дизайнеры, системные администраторы, координаторы технического отдела, системные аналитики, да и программисты-разработчики тоже. Несмотря ни на что, самые настойчивые и упрямые все-таки умудряются добиться потрясающих результатов. И мы с Мариной тоже добились-таки того, к чему стремились! Она стала работать в одной из ведущих компаний по разработке программного обеспечения для компаний сотовой связи, я же устроилась системным администратором в торговый центр. Так что вывод один: при правильном подходе выживет сильнейший — будь это мужчина или женщина, надо только быть настойчивым! Евгения Саблина


женщина и компьютер

Почему их мало в компьютерных компаниях Бурно развивающийся бизнес в сфере компьютерных технологий интересует многих людей. Привлекательность работы в крупной компьютерной фирме состоит из нескольких моментов. Один из них – реальная возможность заработать деньги. Второй – перспектива достаточно быстрого продвижения по служебной лестнице, третий шанс показать себя, проявить свои способности. Именно так или подобным образом отвечают молодые люди в возрасте 22-28 лет на вопрос о причинах, побудивших их к поиску работы в компьютерной компании. Основной состав персонала бурно развивающихся компьютерных фирм – именно такие молодые ребята, со свежими мозгами, оптимизмом и желанием преобразовать окружающий мир. Есть, правда, одна особенность: женщин в такие компании берут на работу достаточно неохотно. Анализ состава компаний говорит о том, что компьютерный бизнес преимущественно мужской. И причин для этого достаточно много. Прежде всего, различна мотивация. Для мужчин важно достижение поставленной цели, они, собственно, для этого и предназначены – преобразовывать окружающий мир. Для женщин же характерно желание сохранения жизни. И довольно часто представительницы прекрасного пола идут на работу в такие компании не от хорошей жизни. Медицинская статистика, в свою очередь, показывает, что, как бы компьютеры не были совершенны, они больше предназначены для мужчин, чем для женщин. В психологическом плане, компьютерный бизнес предполагает больше работу в системе «человек – знак» и требует именно логического мышления - правополушарного. Женщинам же больше свойственно эмоциональное мышление - левополушарное. Даже гормональный состав у мужчин и женщин сильно отличается. Если сравнить количественный

№1, октябрь 2002

состав гормонов чувства и гормонов действия у мужчин и женщин, то картина будет выглядеть примерно так: на одну часть гормонов чувства у мужчин приходится около тысячи частей гормонов действия, а у женщин – с точностью до наоборот: на одну часть гормонов действия приходится в тысячу раз больше гормонов чувства. Получается, что основная деятельность женщин – чувства, а основная деятельность мужчин – действие. И когда мужчина начинает много чувствовать, а женщина – много действовать, нарушается гормональный баланс в организме. Ничего хорошего из этого, как Вы сами понимаете, выйти не может. Да и при рассмотрении ситуации взаимоотношений в группе видно различное отношение женского коллектива к мужчинам, присутствующих в нем, или мужского – к 1-2 женщинам в том же положении. Если женщины в такой ситуации будут чувствовать себя комфортно, то мужчины просто заработают невроз, потому что женщине важно отношение к ней, а мужчине – отношение к тому, что он делает. Специфика работы в компьютерной фирме такова, что часто персоналу приходится работать в стрессовой ситуации (например, сдача проекта, который по каким - либо причинам «не идёт»). А восприятие стресса у мужчин и женщин различно - мозг реагирует по-разному. У женщин в момент опасности, пусть даже гипотетической, в работу включаются все мозговые центры и отключаются только после того, как опасность перестаёт существовать. У мужчин в момент опасности включается только один конкретный мозговой центр, деятельность которого через 7-10 минут угасает, если опасность не подтверждена другими центрами, отвечающими за реагирование на конкретный вид раздражителя. И получается, что мужчины при возникновении стрессовой си-

туации реагируют на ситуацию, а женщины – на раздражитель. Мужчины стараются найти деловое решение, а женщины – выйти из ситуации с минимальными потерями для себя. Ещё одна особенность таких фирм в том, что по роду деятельности программистам часто приходится работать вдвоём, в «тандеме». И от взаимоотношений друг с другом часто зависит результат такой работы. Здесь тоже есть несколько моментов. При работе в паре один из специалистов становится ведущим, другой – ведомым. При этом роли часто меняются. Мужская психика довольно спокойно реагирует на смену ролей, так как дело важнее. А вот женская – нет. Выше я уже писал, что для женщины важно отношение к ней. И по этой причине смена ролей для женщины не всегда происходит безболезненно. Часто эти перемены роли воспринимаются как покушение на безопасность, что, соответственно, может вызвать совершенно непредсказуемую реакцию. По этой причине небольшие компании, состоящие из 5-7 человек, являются достаточно замкнутыми сообществами мужчин, а в крупных компьютерных компаниях женщины занимают должности, связанные с административной работой. Да простят меня воинствующие феминистки, но для женщин есть много рабочих мест, где востребованы замечательные женские качества: сострадание, чувствительность, женская интуиция, умение эффективно работать в монотонном режиме (от которого мужчины сходят с ума), ассоциативное мышление, стремление выжить, умение создать комфорт и уют. Без женщин не обходится ни одна сфера жизнедеятельности, просто давайте заниматься тем, для чего мы предназначены. Вячеслав Михалёв, психолог.

95


анонс №1, Октябрь, 2002 год УЧЕРЕДИТЕЛИ Владимир Положевец Александр Михалев РУКОВОДИТЕЛЬ ПРОЕКТА Петр Положевец РЕДАКЦИЯ Главный редактор Александр Михалев chief@samag.ru Ответственный секретарь Наталья Хвостова sekretar@samag.ru Художник Игорь Усков igus@samag.ru Верстка Владимир Положевец imposer@samag.ru Владимир Лукин maker_up@samag.ru РЕКЛАМНАЯ СЛУЖБА тел./факс:(095) 298-0316 Наталья Хохлова тел.:(095) 928-8253 (доб. 112) Наталья Политыко reklama@samag.ru 103012, г. Москва, Ветошный переулок, дом 13/15 тел.: (095) 928-8253 (доб. 112) факс: (095) 928-8253 Е-mail: info@samag.ru Internet: www.samag.ru ИЗДАТЕЛЬ ЗАО «Редакция «Учительской газеты» Отпечатано ЗАО «Холдинговая компания «Блиц-информ». Образцовая типография «Блицпринт» г. Киев, ул. Довженко, 3 Тираж 5000 экз. Журнал зарегистрирован В Министерстве РФ по делам печати, телерадиовещания и средств массовых коммуникаций (свидетельство ПИ № 77-12542 от 24 апреля 2002 г.)

96

ЧИТАЙТЕ В СЛЕДУЮЩЕМ НОМЕРЕ: Тема номера

БЕЗОПАСНОСТЬ Интервью с начальником «Управления Р» Александром Слуцким Был уже восьмой час вечера. В кабинете начальника отдела по борьбе с компьютерными преступлениями Александра Слуцкого обсуждались детали завтрашнего «оперативного мероприятия». Собирались брать матерого хакера. Когда детали завтрашнего рейда намеченного между прочим на 6 утра были согласованы, Александр Сергеевич извинился, достал из сумки котлету и начал устало ее поглощать. «Вы меня извините, я еще не завтракал». Собственно я и сам уже понял, что работа самого интеллектуального «хакерского» отдела не сахар.

Создание простейшей статистики для Internet Service Providers В большенстве случаев, при создании ISP, самым главным вопросом становится — построение на первых порах простейшего биллинга: подсчет количества времени и денег, затраченных пользователем в Internet’e. В этом нет ничего сверхсложного, но попробуем разобраться, и постараемся создать простейший биллинг. Добавлять что-либо Вы сможете сами или наоборот — убирать: как Вам будет угодно.

Cравнение сетевых сканеров безопасности Сканер безопасности - это программное средство для удаленной или локальной диагностики различных элементов сети на предмет выявления в них различных уязвимостей, которые могут быть использованы

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

МЫТАРСТВА БЛАЖЕННОГО СИСАДМИНА Большинство системных администраторов не выбирают эту карьеру, она предназначена им судьбой. Меня судьба взяла за шкирку и посадила в сисадминское кресло совершенно случайно. Я работал программистом в одной государственной конторе и однажды имел несчастье попасться на глаза шефа в не совсем трезвом состоянии. Дело было в 10 часов утра — самое, что ни на есть, рабочее время. Естественно, на следующий день меня вызвали на ковер и заявили, что быть мне отныне... ни за что не поверите — «системным администратором»!

Продолжение статьи про механизм отражений в Java. «Я постараюсь рассказать о самом нетривиальном механизме мира отражений — создании собственных загрузчиков классов, а также о том, как компилировать новые классы из исходного Java-текста для передачи собственному загрузчику».


Turn static files into dynamic content formats.

Create a flipbook
Issuu converts static files into: digital portfolios, online yearbooks, online catalogs, digital photo albums and more. Sign up and create your flipbook.