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

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

Итак, давайте вспомним, как мы создавали демона:

 #!/usr/bin/perl -w # sited.pl use strict; my $pid = fork; exit if $pid; die "fork() failed" unless defined($pid); use POSIX; die "Can't start new session: $!" unless POSIX::setsid(); 

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

 use Socket; my $logf = "/home/whirlwind/Projects/site/sited.log"; my %proc = ( help => \&help ); my $host = "localhost"; my $port = 10000; my $work = 1; my %pid = (); 

Так как мы отсоединились от терминала, выводит сопроводительные сообщения нам будет некуда. Поэтому мы будем использовать лог-файл, путь к которому определяется переменной $logf. Хэш %proc содержит ссылки на командные процедуры. Более подробно использование хэша %proc мы рассмотрим позднее.

После объявления следует немного видоизмененный код сервера из предыдущей главы:

 my $sock_name = GetSockName($host,$port) or die "Couldn't convert $host into an Internet address: $!\n"; socket(SERVER,PF_INET,SOCK_STREAM,getprotobyname('tcp')) or die "Couldn't create socket: $!\n"; 	 setsockopt(SERVER,SOL_SOCKET,SO_REUSEADDR,1) or die "setsockopt() failed: $!\n"; bind(SERVER,$sock_name) or die "Could't bind to port $port: $1\n"; listen(SERVER,SOMAXCONN); $SIG{HUP} = $SIG{INT} = $SIG{TERM} = \&UsKill; $SIG{CHLD} = \&ReapChld; while ($work){ 	Log("Awaiting incoming connections..."); 	my $rem_addr = accept(CLIENT,SERVER); 	 my ($ip,$pt) = GetSockAddr($rem_addr); 	 Log("Connection from $ip:$pt"); 	Client(); } close(SERVER); Log("Server shutdown"); my @pids = keys(%pid); foreach my $k (@pids){ 	 Log("Waiting process $k..."); 	 waitpid($k,0); } Log(""); 

Все сообщения теперь перенаправлены в лог-файл. Для пущей надежности изменим обработчик сигналов UsKill:

 sub UsKill{ 	 Log("Interrupted by signal: ".shift); 	 $work = undef; 	 return unless socket(KILL,PF_INET, 	 SOCK_STREAM,getprotobyname('tcp')); 	 connect(KILL,$sock_name); 	 close(KILL); 	 $SIG{HUP} = $SIG{TERM} = $SIG{INT} = 'IGNORE'; } 

После получения первого HUP, TERM или INT последующие сигналы будут игнорироваться.

Функция Log() добавлена в отладочных целях. Мне кажется ее код не требует дополнительных пояснений:

 sub Log{ 	 return unless defined ($_[0]); 	 return unless open(LOG,">>$logf"); 	 flock(LOG,2); 	 print LOG scalar(localtime),"[$$]:$_[0]\n"; 	 flock(LOG,8); 	 close(LOG); } 

А теперь самое вкусненькое - функция Client():

 sub Client{ 	my $pid = fork; 	unless (defined($pid)){ 		Log("Fork failed"); 	 	return; 	} 	if ($pid != 0){ # Parent process 		Log("New client process PID=$pid"); 		close CLIENT; 		$pid{$pid} = 1; 		return; 	} 	# False call 	unless($work){ 		close CLIENT; 		exit; 	} 	$SIG{INT} = $SIG{HUP} = $SIG{TERM} = 'IGNORE'; 	# --- Session 	Log("Session start"); 	select(CLIENT); $| = 1; 	my $loop = 1; 	local $SIG{PIPE} = sub{$loop=undef}; 	while ($loop){ 		my $line =  ; 		last unless defined($line); 		chomp($line); 		$line = lc($line); 		last if $line eq 'exit'; 		if (exists($proc{$line})){ 			Log("Call: $line"); 			# Call proc 			&{$proc{$line}}; 		}else{ 			Log("Unknown command: $line"); 			# Unknown command 			print "ER\n"; 		} 	} 	Log("Session end"); 	# --- End of session 	close CLIENT; 	exit; } 

Функция дополнена кодом обработки клиента. Новый код начинается с проверки флага $work. Сделано это для того, что бы процесс, возникший в результате поступления фиктивного подключения (при получении сигнала на завершение), не выполнял работу как для обычного клиента, а завершал работу процедуры.

Следующий оператор определяет поведение при обработке прерывающих сигналов - не обрабатывать.

Мы устанавливаем дескриптор потока вывода по умолчанию, для того, что бы все операции вывода направлялись клиенту. После этого любые процедуры могут спокойно выводить данные как на экран (комментирование этих операторов может помочь во время отладки).

Пока не смотрите на обработчик сигнала PIPE. Общение с клиентом происходит следующим образом: сначала говорит клиент, а потом отвечает сервер и так до посинения. Мы объявляем переменную $loop которая, по аналогии с $work основного цикла сервера, сигнализирует о состоянии цикла обработки запросов. В цикле сервер проверяет полученную строку и сравнивает её с доступными командами. Самая главная команда - это конечно же exit. Как только клиент посылает серверу строку exit, работа с клиентом завершается. Другие команды описаны в хэше %proc (помните, мы добавили одну команду в самом начале программы). Каждой команде сопоставлена определенная процедура. На данный момент сервер поддерживает одну такую команду - help(). Давайте посмотрим на ее код:

 sub help{ 	print "OK\n",join("\n",keys(%proc)),"\nexit\n"; } 

Эта процедура просто выводит список доступных команд. Заметьте, что первая строка ответа сервера содержит код выполнения: "ОК" в случае успешного завершения операции или "ER" в случае ошибки. В нашем сервере возможен только один вариант, когда возвращается "ER" - клиент запросил на выполнение неизвестную команду.

Когда сервер ожидает данные от клиента, работа процесса блокируется. Переход на следующий оператор произойдет либо когда поступят новые данные, либо когда клиент завершит работу. А как же быть, если клиент завершит работу сразу после отправки данных? Ведь командные процедуры, которые будут выполняться после идентификации команды, будут пытаться вывести данные в закрытый сокет. Эту проблему мы решаем переопределением обработчика сигнала PIPE, который приходит после попытки записи в закрытый сокет. Что делает наш обработчик: он просто сбрасывает флаг состояния цикла и вместо перехода к следующей итерации цикл завершит свою работу. К сожалению это не идеальное решение: в случае послупления сигнала PIPE в процессе работы командной процедуры цикл не будет прерван. Это значит, что если в коде командной процедуры будет несколько операторов печати, то все они вызовут посылку PIPE и цикл не завершится пока процедура не закончит свою работу. Как вариант решения этой проблемы можно предложить накопление вывода в переменной и единовременный вывод ответа с помощью одного оператора print.

Пора тестировать наш сервер. Для этого понадобится разработать специального клиента. Так как мы уже рассматривали работу клиентской части я не стану пояснять код:

 

 #!/usr/bin/perl -w $host = 'localhost'; $port = 10000; use IO::Socket; exit unless $socket = IO::Socket::INET->new(PeerAddr	=> $host, 			 PeerPort	=> $port, 			 Proto	=> 'tcp', 			 Type	=> SOCK_STREAM); $socket->autoflush(1); die "Can't fork: $!\n" unless defined($kidpid = fork); if ($kidpid){ 	while(defined($line = <$socket>)){ 	 print STDOUT $line; 	} 	kill("TERM" => $kidpid); }else{ 	print STDOUT "> "; 	while (defined($line =  )){ 	 print $socket $line; 	 last if $line =~ /exit/i; 	} } close($socket); 

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

 Fri Aug 23 14:41:47 2002[20749]:Connection from 127.0.0.1:1657 Fri Aug 23 14:41:47 2002[20749]:New client process PID=20907 Fri Aug 23 14:41:47 2002[20749]:Awaiting incoming connections... Fri Aug 23 14:41:47 2002[20907]:Session start Fri Aug 23 14:41:51 2002[20907]:Unknown command: test Fri Aug 23 14:41:54 2002[20907]:Call: help Fri Aug 23 14:42:09 2002[20907]:Session end Fri Aug 23 14:42:09 2002[20749]:Reap child process 20907 Fri Aug 23 14:42:54 2002[20749]:Interrupted by signal: TERM Fri Aug 23 14:42:54 2002[20749]:Connection from 127.0.0.1:1661 Fri Aug 23 14:42:54 2002[20749]:New client process PID=20911 Fri Aug 23 14:42:54 2002[20749]:Server shutdown Fri Aug 23 14:42:54 2002[20749]:Waiting process 20911... Fri Aug 23 14:42:54 2002[20749]: 

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

 
Автор: Whirlwind
 
Оригинал статьи: http://woweb.ru/publ/58-1-0-434