Сервер-демон на языке perl прослушивающий tcp сокет

После прочтения данной статьи Вы узнаете, как создавать серверы-демоны, что такое Интернет сокеты, процессы-зомби и сигналы.

Демоны

Для начала необходимо отличать понятия обычного процесса, процесса – демона и системного процесса. Все сразу видно по таблице процессов. Запустите команду :

# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.1 0.1 2060 620 ? Ss 08:27 0:00 init [5]
root 2 0.0 0.0 0 0 ? S< 08:27 0:00 [migration/0]
root 5222 0.0 0.2 4532 1412 pts/1 Ss 08:30 0:00 -bash
root 5274 0.0 0.1 4244 928 pts/1 R+ 08:33 0:00 ps aux

Системные процессы - те у которых TTY = ? и VSZ = 0
Демоны – те у которых TTY = ? и VSZ ≠ 0
Пользовательские процессы – те у которых TTY = pts/1(к примеру) и VSZ ≠ 0

Если при запуске Вашей программы Вы не добились TTY = ? и VSZ ≠ 0, то Ваш процесс не является демоном.
Процессы демоны обычно запускаются при загрузке системы и завершаются при завершении работы системы.

Для создания демона необходимы следующие действия - сначала завершить родительский процесс:

$pid = fork();
exit() if $pid;

Затем разорвать связь с управляющим терминалом и создать связь с новым терминалом при помощи команды:

POSIX::setsid();

Сокеты

Описанный в данной статье сервер будет поддерживать Интернет сокет, поэтому дадим определения сокетам.
Сокеты являются "конечными пунктами" в процессе обмена данными.
Обмен данными через сокеты может осуществляться на одном компьютере или через Интернет.
Существуют два самых распространенных типа сокетов: потоковые и датаграмные. Потоковые сокеты обеспечивают двусторонние, последовательные и надежные коммуникации; они похожи на каналы (pipes). Датаграммные сокеты не обеспечивают последовательную, надежную доставку, но они гарантируют, что в процессе чтения сохранятся границы сообщений. (Описание сокетов взято из книги "PERL: Библиотека программиста" Т. Кристиансен, Н. Торкингтон)
Сокеты также делятся по областям(domain): сокеты Интернета и сокеты UNIX. Интернет сокет содержит в себе две составляющие: хост (IP-адрес в определенном формате) и номер порта. UNIX сокеты представляют собой файлы (пример сокета Unix применяется в сервере mysqld socket=/var/lib/mysql/mysql.sock ).

Для создания Интернет сокета на сервере с портом 23 необходима следующая команда:

$server = new IO::Socket::INET(LocalPort => 23,
                                  TYPE => SOCK_STREAM,
                                  Reuse => 1,
                                  Listen => 10);

Такой командой создается потоковый сокет, который будет слушать 23 порт, с 10 подключениями в очереди , и с возможностью использования того же адреса после перезапуска сервера.

Серверы с ветвлением

Для обслуживания запросов от нескольких клиентов необходим сервер с ветвлением. Для этого при каждом входящем подключении от клиента необходимо делать копию родительского процесса (сервера) и "обслуживать" клиента непосредственно ответвленным (скопированным, дочерним) процессом. Делается это при помощи команды fork. Функция fork создает клон текущего процесса. В родительский процесс она возвращает $pid порожденного процесса, а в дочернем процессе возвращаемое значение равно нулю.
Поэтому для того чтобы создать самый простой сервер с ветвлением необходимо сделать следующее:

while($client = $server->accept()) {
    defined(my $child_pid=fork()) or die "Can't fork new child $!";
    ###Родительский процесс идет в конец ###
    ###и ждет следующего подключения ###
    next if $child_pid;
    ###Дочернему процессу копия сокета не нужна ###
    if($child_pid == 0) {
    close($server);
    }
    ###Здесь идет обработка клиентского запроса, ###
    ###выполнение всех необходимых команд ### 
    ....
    exit;### В конце завершаем порожденный процесс ###
    }
continue {
    close($client); ### Не нужно родительскому процессу ### 
}

Процессы-зомби

В сервере с ветвлением при завершении порождаемого процесса (exit) и не завершении родителя появляются процессы-зомби. Процесс-зомби - дочерний процесс в Unix системе, завершивший свое выполнение, но еще присутствующий в таблице процессов. Зомби можно узнать в списке процессов (выводимых утилитой ps) по флагу «Z» в колонке STAT.
Если родительский процесс игнорирует обработчик $SIG{CHLD}, то зомби остаются до завершения родителя. Необходимо добавить функцию отслеживания сигнала $SIG{CHLD} :

sub REAPER {
    while ((my $waitedpid = waitpid(-1,WNOHANG)) > 0) { }
    $SIG{CHLD} = \&REAPER;
}

И перед разветвлением вызвать обработчик

$SIG{CHLD} = \&REAPER;
defined(my $child_pid=fork()) or die "Can't fork new child $!";

Тогда наши наши уже не нужные отработавшие процессы будут корректно завершаться.

Сигналы

%SIG - это хэш ссылок на обработчики сигналов ( ссылки на функции).
Сигнал $SIG{INT} обычно возникает при нажатии Ctrl+C и требует, чтобы процесс завершил свою работу.
Сигнал $SIG{TERM} посылается командой kill при отсутствии явно заданного имени сигнала.
К примеру для обработки сигналов $SIG{INT} и $SIG{TERM} можно написать следующую функцию:

sub signal_handler{
    $time_to_die = 1;
    close($server);
}
$SIG{INT}= $SIG{TERM} = \&signal_handler;

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

$SIG{HUP} = \&rereading_config;
sub rereading_config{
    @def_commands=();
    open(FILECONF,$conf_name) or die "Can't open config file \n";
    while(<FILECONF>){
        chomp;
        push(@def_commands, $_);
    }
    close(FILECONF);
}

Вызвать сигнал HUP для процесса сервера можно так:

kill -s HUP номер процесса

Краткое описание работы telnet сервера-демона

Наш сервер будет работать на 23 порту (слушать 23 порт). Он будет обрабатывать все входящие соединения на этом порту. Если пользователь будет посылать команду, указанную в конфигурационном файле, то сервер будет выполнять её, если же команда будет не из разрешенного списка, то сервер будет её пропускать и переходить к следующей команде. Для того чтобы обновить список разрешенных команд, необходимо серверу послать сигнал HUP.

Пример работы

Запускаем демона на сервере:
[root@server ~]# ./simple-telnetd.pl

Проверяем, что 23 порт прослушивается и ожидает соединения с клиентом:
[root@server ~]# netstat -an|grep LISTEN
tcp 0 0 0.0.0.0:23 0.0.0.0:* LISTEN

Теперь с машины клиента соединяемся по telnet на серевер и вводим команды, которые указаны во входных настройках (/etc/simple-telnetd.conf):

[root@client ~]# telnet 192.168.254.40
Trying 192.168.254.40...
Connected to localhost (192.168.254.40).
Escape character is '^]'.
Command :uname -a
Linux redhat2.ascon.ru 2.6.18-92.el5 #1 SMP Tue Apr 29 13:16:12 EDT 2008 i686 i686 i386 GNU/Linux
Command :uname
Linux
Command :who
root pts/1 2010-01-11 08:30 (192.168.254.1)
Command :

Видим, что программа реагирует корректно. Если ввести не корректную команду, то будет предложено ввести следующую команду.
Если в это время на сервере вывести список установленных соединений, то увидим соединение с клиентом:

[root@server ~]# netstat -an|grep EST
tcp 0 0 192.168.254.40:23 192.168.254.30:49598 ESTABLISHED

Если в это время на клиенте вывести список установленных соединений, то увидим соединение с сервером:

[root@client ~]# netstat -an|grep EST
tcp 0 0 192.168.254.30:49598 192.168.254.40:23 ESTABLISHED

После выхода клиентом из клиентской программы telnet соединение на обоих концах разорвется. Если вызывать клиентскую программу telnet несколько раз, то будут образовываться сразу несколько соединений с сервером, это реализуется с помощью распараллеливания процессов ( копирования самого себя с помощью fork).

Текст программы с комментариями

#!/usr/bin/perl -w
####################
#Shpatserman Maria 
#01.12.2009
#Simple Telnetd 
####################
###Подключение всех необходимых модулей###
use strict;
use POSIX;
use POSIX ":sys_wait_h";
use IO::Socket;
use IO::Handle;

###Создаем процесс-демон###
my $pid= fork();
exit() if $pid;
die "Couldn't fork: $! " unless defined($pid);
###Создаем связь с новым терминалом###
POSIX::setsid() or die "Can't start a new session $!";
###Переменная - бесконечное время жизни сервера###
my $time_to_die =0;
###Переменная - интернет-сокет или сервер###
my $server;
###Функция обработчик  сигналов INT и TERM###
###Она срабатывает перед этими сигналами###
sub signal_handler{
    $time_to_die = 1;
    close($server);
}
$SIG{INT}= $SIG{TERM} = \&signal_handler;
###Файл конфигурации с набором команд, которые обрабатывает наш сервер###
my $conf_name="/etc/simple-telnetd.conf";
###Массив где хранится список этих команд ###
my @def_commands;
###Функция обработчик сигнала HUP перечитывает конфигурационный файл###
###и обновляет массив @def_commands###
$SIG{HUP} = \&rereading_config;
sub rereading_config{
    @def_commands=();
    open(FILECONF,$conf_name) or die "Can't open config file \n";
    while(<FILECONF>){
        chomp;
        push(@def_commands, $_);
    }
    close(FILECONF);
}

###Функция обработчик сигнала  CHLD - для уборки процессов зомби ###
sub REAPER {
    while ((my $waitedpid = waitpid(-1,WNOHANG)) > 0) { }
    $SIG{CHLD} = \&REAPER;
}

###Заполняем массив разрешенных команд при старте сервера###
rereading_config();
###Создаем интернет сокет на порту 23###
my $server_port=23;
$server= new IO::Socket::INET(LocalPort => $server_port,
                                  TYPE => SOCK_STREAM,
                                  Reuse => 1,
                                  Listen => 10)
or die "Couldn't be a tcp server on port $server_port: $@\n";
###Сервер работает до бесконечности пока его не вырубит Term ###
 until($time_to_die){

    my $client;
    ###Обрабатываем входящие подключения
    while($client = $server->accept()){
        ###Включаем обработку зомби###
        $SIG{CHLD} = \&REAPER;
        ###Тот который постучался, отделяем в отдельный процесс###
        defined(my $child_pid=fork()) or die "Can't fork new child $!";
        ###Родительский процесс идет в конец и ждет следующего подключения###
        next if $child_pid;
        ###Дочернему процессу копия сокета не нужна, её закрываем###
        if($child_pid == 0) {
            close($server);
        }
        ###Очистка буфера###
        $client->autoflush(1);
        my $is_def_command=0;
        print $client "Command :";

        ###Считываем комады от клиента построчно###
        while(<$client>){
            ###Если строка пустая переходим в конец блока###
            next unless /\S/;
            ###Запоминаем полную введенную строку, к примеру df -h ###
            my $full_enter_str = $_;
            chomp($full_enter_str);
            ###Переменная – имя команды, к примеру df###
            my $enter_command="";
            ###Переменная – набор параметров, к примеру -h### 
            my $enter_params="";
            ###Разбиваем введенную строку на имя команды и параметры###
            ###########################################################
            if($full_enter_str =~ /(\w+)(\s+)(.*)(\s*)/){
                $enter_command = $1;
                $enter_params = $3;
            }
            elsif($full_enter_str =~ /(\w+)/){
                $enter_command = $1;
                $enter_params = "";
            }
            else {
                $enter_command = "";
                $enter_params = "";
            }

            ###Сравнение имени команды с набором разрешенных команд ###
            ###Просматриваем разрешенные команды в конфигурационном файле ###
            foreach (@def_commands) {
                if($enter_command eq $_) { $is_def_command=1;}
            }


            ###Если команда разрешена — выполняем её###
            ###########################################
            if($is_def_command){
                my @lines = qx($enter_command $enter_params);
                foreach (@lines){
                    print $client $_;
                }
            }

        }
        continue {
            print $client "Command :";
            $is_def_command=0;

        }
    exit;
    }
    continue {
        close($client);
    }

}

Исходный текст программы: simple-telnetd.pl

Пример входных настроек: simple-telnetd.conf

ЗЫ хороший мануал perldoc perlipc