mysyslog.ru

21 Декабрь 2008

tcp сервер на perl

написано в рубрике: programming — Метки: , — constantine.malov @ 0:34

Для построения распределенных систем есть несколько часто используемых протоколов, например SOAP или XML-RPС. Можно придумать что-то свое или использовать базу данных. В любом случае для сетевого взаимодействия понадобится какая-то служба. Тут тоже можно изобрести велосипед и написать свой tcp сервер. В данной статье я расскажу как написать свой tcp сервер на perl.

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

Поиски в гугле ничего интересного не дал, было несколько интересных ссылок с примерами, но все они были без детального объяснения. Потому я решил подойти к вопросу с другой стороны, и стал искать статьи о сетевом программировании на perl. Через какое-то время я нашел книгу “Сетевое программирование на perl” (Network Programming with Perl) Линкольна Стина (Lincoln D. Stein), в которой было все очень хорошо и досконально написано. Дальнейшее повествование – вольный пересказ соответствующих глав книги.

Сперва немного теории. Мы будем писать forking сервер, т.е. наш сервер будет представлять из себя родительский процесс, порождающий дочерние процессы для обработки каждого нового соединения. Есть и другие варианты реализации сетевых служб: мультипоточные и мультиплексные сервера. Но мы их касаться не будем.

Расскажу о логике работы нашего forking сервера.

  1. Родительский процесс создает сокет и ждем входящие соединения от клиентов
  2. Если клиент подключается к сокеты, то родительский процесс принимает соединение.
  3. Сама интересная часть. После установления соединения, родительский процесс порождает дочерний, который является точной копией родительского, т.е. в нем есть и только что принятое соединение и значит дочерний процесс его может обработать.
  4. Родительский процесс у себя закрывает открытое соединение (его теперь обрабатывает его ребенок) и возвращается к ожиданию нового подключения. Если приходит новый клиент – повторяются пункты 1-4.
  5. А в это время дочерний процесс обменивается данными с клиентом. Когда говорить уже им не о чем, процесс завершает свою работу.

Звучит все достаточно просто? На деле все как всегда несколько иначе.  И виноваты в этом зомби. Дело в том, что fork() создает из родительского процесса точную его копию. Так появляется дочерний процесс, который может существовать отдельно от родителя. Но связь между ними остается. Все как в жизни.

Когда дочерний процесс завершает свою работу, то он не исчезает на самом деле. Он остается в списке процессов системы до тех пор, пока родитель не считает его код завершения работы.  Для этого используются вызовы wait() и waitpid(). Если родитель не будет использовать эти вызовы, то рано или позно таблица процессов ОС переполнится и ядро перестанет создавать новые процессы. А нам такой работы не нужно.  Потому родительский процесс должен корректно обрабатывать прекращение работы его потомков, более того, знать чем закончилась работа потомка полезно.

Здесь мы плавно переходим к теме обработки сигналов. Помните я говорил о связи между родительским и дочерним процессом? Для этой связи используются сигналы. Если статус любого ребенка меняется, то родителю отсылается сигнал CHLD . Причем какой имеено дочерний процесс прекратил свое существование сигнал не сообщает, только вызов wait() родительским процессом позволит определить ребенка.

Теперь попробуем собрать все вместе в коде:

  1. #!/usr/bin/perl -w
  2.  
  3. use strict;
  4. use IO::Socket;
  5. use POSIX ‘WNOHANG’;
  6.  
  7. my $port = 3333;
  8. my $bind = ‘127.0.0.1′;
  9. my $work = 1;
  10.  
  11. $SIG{INT} = sub { $work–; };
  12. $SIG{CHLD} = sub { while ( waitpid(-1,WNOHANG) > 0 ) {} };
  13.  
  14. my $lsocket = IO::Socket::INET->new(
  15.         LocalPort => $port,
  16.         Listen => 20,
  17.         Proto => ‘tcp’,
  18.         Bind => $bind,
  19.         Reuse => 1,
  20.         Timeout => 3600,
  21. );
  22.  
  23. if (!$lsocket) {
  24.         die "Can’t create socket";
  25. }
  26.  
  27. while ($work) {
  28.         next unless my $connection = $lsocket->accept;
  29.  
  30.         defined (my $pid = fork()) or die "Can’t fork new child: $!";
  31.         if ($pid == 0) {
  32.                 $lsocket->close;
  33.                 serve_client($connection);
  34.                 exit 0;
  35.         }
  36.  
  37.         $connection->close;
  38. }
  39.  
  40. sub serve_client {
  41.         my $sock = shift;
  42.         STDIN->fdopen($sock, "<") or die "Can’t reopen STDIN: $!";
  43.         STDOUT->fdopen($sock, ">") or die "Can’t reopen STDOUT: $!";
  44.         STDERR->fdopen($sock, ">") or die "Can’t reopen STDERR: $!";
  45.         $|=1;
  46.         print(STDOUT "Hello!n");
  47.         sleep 10;
  48. }

Давайте разберем код. В строчках 1-9 подключаются нужные модули и объявляется несколько переменных. Из интересного здесь use POSIX ‘WNOHANG’. Эта константа будет использоваться в флагах waitpid().
Строчки 11-12 примечательны. Здесь мы добавляем обработку сигналов INT позволяет нам “красиво” завершить работу сервера. С CHLD все несколько сложнее. Дело в том, что CHLD посылается родительскому процессу без указания pid процесса, завершившего свою работу. Потому waitpid() вызывается с параметром -1, т.е. обработке подлежит любой дочерний процесс, завершивший свою работу. Более того, может случиться так, что одновременно завершит свою работу два или более дочерних процессов, а систему сигналов UNIX в единицу времени может отослать только один сигнал. Потому если просто запустить waitpid(), не все процессы могут быть обработаны… Чтобы справится с этим waitpid() запускается в цикле пока возвращает положительные значения. Флаг WNOHANG делает вызов waitpid() неблокирующим, т.е. скрипт не будет ожидать, пока появится завершивший свою работу дочерний процесс.
В строчках 14-25 нет ничего сложного, открывается сокет для прослушивания соединений.
В цикле 27-35 сосредоточена вся работа сервера. Родительский процесс ожидает подключения (вызов accept()), если поступает новое соединение выполняется вызов fork(). Родительский процесс отличается от дочернего переменной $pid. У дочернего она равна 0. Этим можно воспользоваться, чтобы отделить код родителя и потомков. Если $pid == 0, то мы закрываем сокет(его все равно слушает родитель) и вызываем функцию serve_client(), о ней чуть позже. После чего завершаем работу потомка. Если же $pid > 0, то мы опять оказываемся в коде родителя. Здесь нам нужно закрыть соединение, его обрабатывает дочерний процесс. После возвращаемся в начало цикла.
Функция serve_client() в строчках 40-48 содержит собственно общение с клиентом, из важного здесь – переоткрытие потоков stderr stdin srdout на сокет, получаемый через аргумент функции.
На этом все – получаем рабочий сервер на perl.
А теперь зачем так не нужно было делать. На свете есть масса того, что еще стоит написать, зачем же переписывать одно и тоже по сто раз? Это как раз тот случай. Во-первых есть Net::Server::Fork, во вторых есть inetd, который за вас переоткрывает потоки ввода вывода и позволяет забыть о всей сетевой части, сосредоточившись только на логике вашего приложения.

Нет комментариев »

Еще нет комментариев.

RSS лента комментариев к этой записи.

Оставить комментарий

Вы должны войти чтобы оставить комментарий.

Работает на WordPress