Глава 17 Сокеты

17.15. Создание сервера-демона

Проблема

Вы хотите, чтобы ваша программа работала в качестве демона.

Решение

Если вы - параноик с правами привилегированного пользователя, для начала вызовите chroot для безопасного каталога:
chroot("/var/daemon")
or die "Couldn't chroot to /var/daemon: $!";

Вызовите fork и завершите родительский процесс.
$pid = fork;
exit if $pid;
die "Couldn't fork: $!" unless defined($pid);

Разорвите связь с управляющим терминалом, с которого был запущен процесс, - при этом процесс перестает входить в группу процессов, к которой он принадлежал.
use POSIX;
POSIX::setsxd()
or die "Can't start a new session: $!";

Перехватывайте фатальные сигналы и устанавливайте флаг, означающий, что мы хотим корректно завершиться:
$time_to_die = 0;
sub signal_handler { $time_to_die = 1;
}
$SIG{INT} = $SIG{TERM} - $SIG{HUP} = \&signal_handler;
# Перехватить или игнорировать $SIG{PIPE}

Настоящий код сервера включается в цикл следующего вида:
until ($time_to_die) {
# ...
}

Комментарий

До появления стандарта POSIX у каждой операционной системы были свои средства, с помощью которых процесс говорил системе: "Я работаю в одиночку; пожалуйста, не мешайте мне". Появление POSIX внесло в происходящее относительный порядок. Впрочем, это не мешает вам использовать любые специфические функции вашей операционной системы.
К числу этих функций принадлежит chroot, которая изменяет корневой каталог процесса (/). Например, после вызова chroot "/var/daemon" при попытке прочитать файл /etc/passwd процесс в действительности прочитает файл /var/ daemon/etc/passwd. Конечно, при
Операционная система предполагает, что родитель ожидает смерти потомка. Для нашего процесса-демона это не нужно, поэтому мы разрываем наследственные связи. Для этого программа вызывает fork и exit, чтобы потомок не был свя-tan с процессом, запустившем
Все почти готово. Сигналы типа SIGINT не должны немедленно убивать наш процесс (поведение по умолчанию), поэтому мы перехватываем их с помощью %SIG и устанавливаем флаг завершения. Далее главная программа работает гю принципу: "Пока не убили, что-то д Сигнал SIGPIPE - особый случай. Получить его нетрудно (достаточно записать что-нибудь в манипулятор, закрытый с другого конца), а по умолчанию он ведет себя довольно сурово (завершает процесс). Вероятно, его желательно либо проигнорировать ($SIG{PIPE} = '

> Смотри также -------------------------------
Страницы руководства setsid(2) и chroot(l) вашей системы (если есть); описание функции chroot в perlfunc(1).

17.16. Перезапуск сервера по требованию

Проблема

При получении сигнала HUP сервер должен перезапускаться, по аналогии i inetd или httpd.

Решение

Перехватите сигнал SIGHUP и перезапустите свою программу:
$SELF.= "/usr/local/libexec/myd"; # Моя программа
@ARGS = qw(-l /var/log/myd -d); # Аргументы
$SIG{HUP} = \&phoenix;
sub phoenix {
# Закрыть все соединения, убить потомков и
# приготовиться к корректному возрождению.
exec($SELF, OARGS) or die "Couldn't restart: $!\n";
}

Комментарий

Внешне все выглядит просто ("Получил сигнал HUP - перезапустись"), но на самом деле проблем хватает. Вы должны знать имя своей программы, а определить его непросто. Конечно, можно воспользоваться переменной $0 модуля FindBin. Для нормальных программ этого
Обработчик сигнала обязательно должен устанавливаться после определения $SELF и @ARGS, в противном случае может возникнуть ситуация перехвата - SIGHUP потребует перезапуска, а вы не будете знать, что запускать. Это приведет к гибели вашей программы. Некоторые серверы при получении SIGHUP не должны перезапускаться - они всего лишь заново читают свой конфигурационный файл:
$CONFIG_FILE = "/usr/local/etc/myprog/server_conf.pl";
$SIG{HUP} = \&read_config;
sub read_config { do $CONFIG_FILE;
}

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

> Смотри также -------------------------------
Описание функции ехес в perlfunc(1); рецепты 8.16-8.17; 16.15.

17.17. Программа: backsniff

Программа backsniff регистрирует попытки подключения к портам. Она использует модуль Sys::Syslog, а ему, в свою очередь, нужна библиотека syslog.ph, которая не обязательно присутствует в вашей системе. Попытка подключения регистрируется с параметрами LOG_
В системном журнале появляются записи:
May 25 15:50:22 coprolith snifter: Connection from 207.46.131.141 to 207.46.130.164-.echo
В файл inetd.conf включается строка следующего вида:
echo stream tcp nowait nobody /usr/scripts/snfsqrd snifter
Исходный текст программы приведен в примере 17.7. Пример 17.7. backsniff
#!/usr/bin/perl -w
# backsniff - регистрация попыток подключения к определенным портам
use Sys::Syslog;
use Socket;
# Идентифицировать порт и адрес
$sockname = getsockname(STDIN)
or die "Couldn't identify myself: $!\n":
(Sport, $iaddr) = sockaddr_in($sockname);
$my_address = inet_ntoa($iaddr);
# Получить имя службы
$service = (getservbyport (Sport, "tcp"))[OJ || Sport;
# now identify remote address
$sockname = getpeername(STDIN)
or die "Couldn't identify other end: $!\n";
(Sport, $iaddr) = sockaddr_in($sockname);
$ex_address = inet_ntoa($iaddr);
# Занести информацию в журнал openlog("sniffer", "ndelay", "daemon");
syslog("notice", "Connection from %s to %s:%s\n", $ex_address,
$my_address, $service);
closelog();
exit;

17.18. Программа: fwdport

Предположим, у вас имеется защитный брандмауэр (firewall). Где-то в окружаю щем мире есть сервер, к которому обращаются внутренние компьютеры, но дос-туп к серверу разрешен лишь процессам, работающим на брандмауэре. Вы не хо тите, чтобы при каждом обращен Например, такая ситуация возникает, когда Интернет-провайдер вашей ком пании позволяет читать новости при поступлении запроса с брандмауэра, но от вергает все подключения NNTP с остальных адресов. Вы как администратор брандмауэра не хотите, чтобы на нем р
Программа fwdport из примера 17.8 содержит общее решение этой проблемы. Вы можете запустить любое количество экземпляров, по одному для каждого внешнего запроса. Работая на брандмауэре, она общается с обоими мирами. Когда кто-то хочет воспользоваться
Например, командная строка может выглядеть так:
% fwdport -s nntp -I fw.oursite.com -r news.bigorg.com

Это означает, что программа выполняет функции сервера NNTP, прослушивая локальные подключения на порте NNTP компьютера fw.oursite.com. При поступлении запроса она связывается с news.bigorg.com (на том же порте) и организует обмен данными между удаленн Рассмотрим другой пример:
% fwdport -I myname:9191 -г news.bigorg.com:nntp

На этот раз мы прослушиваем локальные подключения на порте 9191 хоста myname и связываем клиентов с удаленным сервером news.bigorg.com через порт NNTP.
В некотором смысле fwdport действует и как сервер, и как клиент. Для внешнего сервера программа является клиентом, а для компьютеров за брандмауэром - сервером. Эта программа завершает данную главу, поскольку в ней продемонстрирован практически весь и Пример 17.8. fwdport
#!/usr/bin/perl -w
# fwdport - прокси-сервер для внешних служб
use strict; # Обязательные объявления
use Getopt::Long; # Для обработки параметров
use Net::hostent; # Именованный интерфейс для информации о хосте
use 10::Socket; # Для создания серверных и клиентских сокетов
use POSIX ":sys_wait_h"; # Для уничтожения зомби
mу (
%Children,
$REMOTE,
$LOCAL,
$SERVICE,
$proxy_server,
$ME,
# Хэш порожденных процессов
# Внешнее соединение
# Для внутреннего прослушивания
# Имя службы или номер порта
# Сокет, для которого вызывается accept()
# Базовое имя программы
);
($МЕ = $0) =~ s,,*/,,; # Сохранить базовое имя сценария
check_args(), # Обработать параметры
start_proxy(); # Запустить наш сервер
service_clients(); # Ждать входящих подключений
die "NOT REACHED"; # Сюда попасть невозможно
# Обработать командную строку с применением расширенной версии
# библиотеки
getopts. sub check_args { Get0ptions(
"remote=s" => \$REMOTE,
"local=s" => \$LOCAL,
"service=s" => \$SERVICE, )
or die "EOUSAGE;
usage: $0
[ --remote host ]
[ --local interface ]
[ --service service ] EOUSAGE
die "Need remote" unless $REMOTE;
die "Need local or service" unless $LOCAL || $SERVICE;
}
# Запустить наш сервер
sub start_proxy {
my @proxy_server_config = (
Proto => 'tcp',
Reuse => 1,
Listen => $OMAXCONN,
):
push @proxy_server_config, LocalPort => $SERVICE if SSERVICE:
push @proxy_server_config, LocalAddr => $LOCAL if $LOCAL;
$proxy_server = 10::Socket::INET->new(@proxy_server_config)
or die "can't create proxy server: $@";
print "[Proxy server on ", ($LOCAL || $SERVICE), " initialized.]\n'
}
sub service_clients { my (
$local_client, # Клиент, обращающийся к внешней службе
$lc_info, # Имя/порт локального клиента
$remote_server, # Сокет для внешнего соединения
@rs_config, # Временный массив параметров удаленного сокета
$rs_info, # Имя/порт удаленного сервера
$kidpid, # Порожденный процесс для каждого подключения
}
$SIG{CHLD} = \&REAPER; ft Уничтожить зомби acceptingO:
# Принятое подключение означает, что внутренний клиент Н хочет выйти наружу
while ($lpcal_client = $proxy_server->accept()) { $lc_info = peennfo($local_client);
set_state("servicing local $lc_info"):
printf "[Connect from $lc_info]\n";
(ars_config = (
Proto => 'tcp',
PeerAddr => $REMOTE, );
push(@rs_conflg, PeerPort => $SERVICE) if SSERVICE:
print "[Connecting to $REMOTE...":
set_state("connecting to $REMOTE"): # См. ниже
$remote_server =

I0::Socket::INET->new(@rs_config) or die "remote server: $@":
print "done]\n":
$rs_info = peerinfo($remote_server);
set_state("connected to $rs_info"):
$kidpid = fork();
die "Cannot fork" unless defined $kidpid;
if ($kidpid) {
$Children{$kidpid} = time(); . # Запомнить время запуска
close $remote_server; # Не нужно главному процессу
close $local_client; # Тоже
next; # Перейти к другому клиенту
}
# В этой точке программа представляет собой ответвленный
# порожденный процесс, созданный специально для входящего
# клиента, но для упрощения ввода/вывода нам понадобится близнец.
close $proxy_server; # He нужно потомку
$kidpid = fork():
die "Cannot fork" unless defined $kidpid;
# Теперь каждый близнец сидит на своем месте и переправляет
# строки данных. Видите, как многозадачность упрощает алгоритм
# Родитель ответвленного процесса, потомок главного процесса
if ($kidpid) {
set_state("$rs_info --> $lc_info");
select($local_client); $| = 1:
print while <$remote_server>;
kill('TERM', $kidpid); # Работа закончена,
} # убить близнеца
# Потомок потомка, внук главного процесса
else {
set_state("$rs_info <-- $lc_info");
select($remote_server); $| = 1:
print while <$local_client>:
kill('TERM', getppidO); # Работа закончена,
} # убить близнеца
exit; # Тот, кто еще жив, умирает
} continue {
accepting( );
}
}
# Вспомогательная функция для получения строки в формате ХОСТ:ПОРТ
sub peerinfo {
my $sock = shift;
my $hostinfo = gethostbyaddr($sock->peeraddr);
return sprintf("%s:%s",
$hostinfo->name || $sock->peerhost, $sock->peerport):
}
# Сбросить $0, при этом в некоторых системах "ps" выдает и нечто интересное: строку, которую мы присвоили $0! sub set_state { $0 = "$МЕ [@]" }
# Вспомогательная функция для вызова set_state sub accepting {
set_state("accepting proxy for " . ($REMOTE || $SERVICE)):
}
# Кто-то умер. Уничтожать зомби, пока они остаются.
# Проверить время их работы. sub REAPER { my $child;
my $start;
while (($child = waitpid(-1,WNOHANG)) > 0)
{ if ($start = $Children{$child}) { my $runtime = time() - $start;
printf "Child $child ran %dm%ss\n", $runtime / 60, $runtime % 60;
delete $Children{$child};
} else {
print "Bizarre kid $child exited $?\n";
}
}
# Если бы мне пришлось выбирать между System V и 4.2, # я бы уволился. - Питер Ханиман
$SIG{CHLD} = \&REAPER;
};


> Смотри также
Getopt::Long(3), Net::hostent(3), IO::Socket(3), POSIX(3), глава 16, раздел "Написание двусторонних клиентов" этой главы.
© copyright 2000 Soft group

Используются технологии uCoz