Существует некоторый класс проблем, которые требуют автоматической проверки наличия удаленного сервиса, слушающего определенный порт. Например, необходимо узнать, когда будет запущен сервис на удаленной машине, или, например, когда будет запущен некий внутренний сервис. Или, в случае неустойчивой связи(удаленный сервер через мобильный телефон по GPRS в деревне, на котором необходимо поддерживать непрерывную связь), нужно отслеживать наличие соединения и по его исчезновению информировать находящихся поблизости людей о том, что связи нет(с случае когда сотовй телефон не умеет сам находясь, на crontab'е, восстанавливать соединение).
Задача сводится к написанию программы, умеющей автоматически опрашивать удаленный порт и при наличии соединения редиректить информацию, посылаемую на этот порт на другие определенные порты в зависимости от заголовка. Аналогичное решение ssh - тунеллирование.
Предположим есть некоторая запускаемая каждое утро программа, обслуживающая некоторое хитрое устройство(например мониторинг скорости водостока или химический анализ речной воды на нескольких участках реки, до и после сточного коллектора того или иного предприятия) в далекой сельской глубинке(где и не пахло связью), с которым необходимо работать одновременно нескольким городским пользователям.
Если программа, обслуживающая устройство, пишет данные в сокет, открываемый ею без указания опции SO_REUSEADDR, то после каждого соединения приходится ждать некоторе время, пока *nix разблокирует сокет для дальнейшей работы после предыдущего соединения. Это замедляет работу и чтобы обойти это препятствие необходимо сделать редиректор, обходящий блокировку сокета. Приведенная ниже программа является промежуточной между программой, управляющей неким устройством(типа описанных выше) и клиентскими приложениями.
Схема работы:
устройство <-> описываемое здесь промежуточное клиент-серверное приложение <-> клиенты
В случае отсутствия соединения в системе постоянно висит процесс(запускаемый например как service redirect start или ./redirect.pl &), умеющий плодить дочерний процесс, опрашивающий удаленный хост. В случае неудачного соединения процесс завершается и порождается заново например через каждые 2 секунды. В случае соединения клиент логинится на удаленный сервер и плодит пул процессов-серверов(для ускоренной обработки соединений), к которым далее могут подсоединяться многочисленные клиенты.
Итак, базовый процесс подсоединился к удаленному клиенту, породил дочерние процессы и готов к работе. Между исходным устройством и подсоединяющимися клиентами возможен обмен данными и задача сводится к обмену данными между разными процессами в памяти *nix-машины.
Для этого необхожимо открыть канал между процессами. Если обмен данными невелик, то можно обойтись одной общей переменной на несколько процессов и изменять её содержимое и соответственно отслеживать её состояние. Можно поступить и по иному, открыть каналы в виде дескрипторов и контролировать таким образом чтение и запись.
#!/usr/bin/perl -w use strict; use IO::Socket; use IO::Handle; use Socket; use Symbol; use POSIX; use Net::hostent; # порт, на котором висит программа, управляющая устройством my $port=6001; # хост my $host="127.0.0.1"; # число плодящихся серверов my $PREFORK="1"; # число подключаемых клиентов my $MAX_CLIENTS_PER_CHILD="1"; # хеш потомков my %children=(); # численность порожденных потомков my $children=0; my $f="/.2/mnt/work/control.txt"; # начальное заполнение пула серверов make_new_child() for(1 .. $PREFORK); # обработчик сигнала CHLD, убивающиего потомка $SIG{CHLD}=\&REAPER; # обработчик события, убивающего всю программу(остановка сервера) $SIG{INT}=\&HUNTSMAN; # поддерживаем численность потомков while(1){ sleep; for(my $i=$children; $i<$PREFORK; $i++){ make_new_child() } } sub make_new_child{ my($pid, $sigset, $kidpid, $server, $client); $sigset=POSIX::SigSet->new(SIGINT); sigprocmask(SIG_BLOCK, $sigset) or die "can't block SIGINT for fork: $!\n"; die "fork: $!" unless defined do{$pid = fork}; if($pid){ # базовый родительский процесс, содержимое этого условия никогда не # будет видно из дочернего процесса sigprocmask(SIG_UNBLOCK, $sigset) or die "can't unblock SIGINT for fork: $!\n"; # заполняем хеш номерами дочерних процессов $children{$pid}=1; # увеличивам число процессов, обрабатывающих подключение $children++; return; } else { # порожденный дочерний процесс, содержимое этого процесса никогда не # будет видно в базовом процессе # переопределяем событие SIGINT чтобы избежять появления зомби $SIG{INT} = 'DEFAULT'; $SIG{CHLD}='IGNORE'; sigprocmask(SIG_UNBLOCK, $sigset) or die "can't unblock SIGINT for fork: $!\n"; # плодим клиентов, соединяющихся с # исходной программой for(my $i=0; $i<$MAX_CLIENTS_PER_CHILD; $i++){ sleep 2; # открываем двусторонний сокет между процессами # парент пишет в CHILD, который читается в порожденном процессе # Дочерний процесс пишет в PARENT, который читается в родительском процессе socketpair(CHILD, PARENT, AF_UNIX, SOCK_STREAM, PF_UNSPEC) or die "socketpair: $!"; # отключаем буверизацию у обоих CHILD->autoflush(1); PARENT->autoflush(1); # соединяемся с исходной программой, в случае отказа # в соединении(это значит что удаленная мониторинговая или иная # программа не работает), дочерний процесс умирает, # и скрипт заново порождает новый процесс в цикле while(1){ ... } my $handle = IO::Socket::INET->new( Proto => 'tcp', PeerAddr => $host, PeerPort => $port) or die "can't connect to port $port on $host: $!"; print STDERR "[Connected to $host:$port]\n"; die "can't fork: $!" unless defined do{$kidpid = fork()}; # в случае соединения клиента с базовой программой # делимся на два процесса, между котороыми окрываем двухсторонний сокет if ($kidpid) { my ($byte, $tmp); # выходим из парента, так как в паренте нужено писать только в чайлд close PARENT; # побайтовое чтение из сокета, в данном случае удаленная программа на # внешние команды возвращает либо слово ok, либо error while(sysread($handle, $byte, 1) eq 1){ $tmp.=$byte; do{print CHILD "$tmp\n"; $tmp='' } if $tmp eq 'ok'; do{print CHILD $tmp,"\n"; $tmp=''} if $tmp eq 'error'; } close CHILD; # при завершении подпроцесса убиваем сервер и избегаем появления зомби kill "TERM" => $kidpid; waitpid($kidpid,0); } else { # выходим из парента, так как в паренте нужно писать только в чайлд close CHILD; # создаем сервер, транслирующий ответы базовой программы клиентам # и передающий команды клиентов исходной программе $server = IO::Socket::INET->new( LocalPort => 2003, Type => SOCK_STREAM, Proto => 'tcp', Reuse => 1, Listen => 10); die "making socket: $@" unless $server; # слушаем, что пишет в ответ сервер while($client=$server->accept()){ # отключаем буферизацию $client->autoflush(1); my ($line, $line1); # если есть данные от клиента while (defined ($line = <$client>)){ # то пишем их в клиента, работающего с внешним устройством print $handle $line; # в случае ответа внешнего устройства через CHILD передаем ответ клиенту chomp($line1=<PARENT>); # пишем пользователю ответ из парента через межпроцессный сокет чайлд/парент # ответ устройства, соответственно ok или error, зависимости от того, # прошла ли команда или нет print $client "$line1\n"; } close $client; # закрываем соединение в случае конца работы пользователя } # выходим из двусотроннего сокета со стороны чайлда, который писал в # дексриптор PARENT close PARENT; } # выходим из цикла порождающих пул процессов программ дабы не переполнить # систему процессами, которые начнут без exit ветвится пока истинно while (1){} } exit; } } # убиваем всех чайлдов сразу, в случае например, перезапуска компьютера sub HUNTSMAN{ local($SIG{CHLD})='IGNORE'; kill 'INT' => keys %children; exit; } # убиваем конкретного чайлда, в случае неработоспособности устройства, # с котороым надо коннектится клиентам... далее по смыслу перезапус через 2 секунды sub REAPER{ $SIG{CHLD}=\&REAPER; my $pid = wait; $children--; delete $children{$pid}; }
В принципе данные решения и идеи без особых затрат и при надлежащем умении позволяют организовать и поддерживать целые сегменты сети без дорогостоящей прокладки оптоволоконных кабелей или дорогостоящих спутниковых приемников при условии полного отсутствия телефонных линий.
Какова цена этого мероприятия? 0.30$/Mb - GPRS BeeLine и 0.15$/Mb GPRS MTS Прокладка же оптоволоконного кабеля и последующая разводка стоит около 20000$, примерно такая-же стоимость и у спутниковой тарелки. Конечно, иногда проложить телефонный кабель дешевле, но это зависит от конкретных условий... А построенная на такого типа серверах-клиентах распределенная мониторинговая система обходится на порядки дешевле. И, к тому-же, без значительных изменений, кроссплатформенна, так как данный скриптовый язык работает на многих типах операционных систем.
В принципе, если написать обработчик входящих писем и отправку почтовых сообщений через sms.beeline.ru на мобильный телефон, можно через сотовый телефон посредством получения и отправки sms-сообщений осуществлять мониторинг и администрирование удаленного сервера(соответственно отпадает необходимость в сотовом notebooks).