PHP-демон с дочерними процессами

Vict0r

Новичок
PHP-демон с дочерними процессами

Здравствуйте. Обращаюсь к вам, PHP-гуру, т.к. сам зашел в тупик. Стоит задача написать демона на PHP, который бы принимал входящие соединения, некоторые из которых оставлял бы активными и периодически отсылал в них какие-то данные, а в некоторые отправлял бы данные и сразу же их закрывал. Сама по себе задача очень простая, но есть одна загвоздка. Клиенты, при подключении, передают демону кодовое слово (например, «big» или «small»). В зависимости от этого слова демон должен послать клиенту маленький объем данных (порядка сотен байт), либо большой объем данных (порядка десятков килобайт). Запросы со словом «small» идут очень часто, в то время как запросы на вывод большого объема данных приходят намного реже. Эффективно обрабатывать такие запросы одним процессом невозможно, возникает необходимость переложить вывод больших объемов данных на дочерние процессы, в то время как процесс-родитель будет обрабатывать «быстрые» запросы. Т.е…

1. На процесс-родитель приходит запрос со словом «small».
2. Процесс-родитель обрабатывает его, а дальше либо оставляет соединение открытым для дальнейшего взаимодействия, либо закрывает его.

1. На процесс-родитель приходит запрос со словом «big».
2. Процесс-родитель создает дочерний процесс, а сам продолжает принимать входящие соединения и обрабатывать запросы на вывод малых объемов данных.
3. Дочерний процесс выводит клиенту большой объем данных, закрывает соединение с клиентом и умирает.

Упрощенно, алгоритм такой:

PHP:
socket_create();
socket_bind();
socket_listen();

while( true )
{
   socket_select();
   socket_accept();
   socket_read();

   if( “small” )
   {
      /* обработка и отправка данных */
      socket_close();
   }
   elseif( “big” )
   {
      pcntl_fork();
      /* на этом месте тупик */
   } 
}
Все, о чем я прошу – это подсказать, как правильно составить алгоритм взаимодействия процесса-родителя и дочернего процесса с клиентом. Т.е. в какой момент родителю надо сделать pcntl_fork(), что родителю делать потом с текущим соединением и как корректно дочернему процессу обработать запрос и умереть. Уже долгое время изучаю материалы на данную тему, но никак не пойму, как это все сделать. Нужны идеи. Заранее благодарен за любую полезную информацию.
 

dr-sm

Новичок
(порядка десятков килобайт) это совсем немного
SO_SNDBUF поставь по максимуму.
еще socket_select поизучай ).
 

Vict0r

Новичок
Спасибо, но суть вопроса не в том, что нужно, чтобы обрабатывать все запросы одним процессом. Это я сделал и демон работает. Стоит задача граммотно переложить вывод большого объема данных на дочерние процессы. Потому что, если начинается передача большого объема данных (скажем, 60 Кб) какому-то клиенту через низкоскоростное соединение со скоростью, скажем 6 Кбайт/сек, то в течение 10 секунд для клиентов с "быстрыми" запросами, демон будет "висеть". А это недопустимо - запросы должны обрабатываться непрерывно с максимальной скоростью.
 

dr-sm

Новичок
http://ru2.php.net/manual/ru/function.socket-select.php

int socket_select ( array &$read , array &$write , array &$except , int $tv_sec [, int $tv_usec ] )

> write - The sockets listed in the write array will be watched to see if a write will not block.
 

Wicked

Новичок
Стоит задача граммотно переложить вывод большого объема данных на дочерние процессы. Потому что, если начинается передача большого объема данных (скажем, 60 Кб) какому-то клиенту через низкоскоростное соединение со скоростью, скажем 6 Кбайт/сек, то в течение 10 секунд для клиентов с "быстрыми" запросами, демон будет "висеть".
вот для этого и нужны неблокирующие сокеты и socket_select().
 

Alexandre

PHPПенсионер
Стоит задача граммотно переложить вывод большого объема данных на дочерние процессы.
ну и в чем проблема, форкаешь процесс, он и будет отдавать твои данные.

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

dr-sm

Новичок
а зачем форкать процесс если можно не форкать (это риторический вопрос) :)
 

Alexandre

PHPПенсионер
вот для этого и нужны неблокирующие сокеты и socket_select().
честно говоря я не понял смогут ли спасти в этом случае неблокирующие сокеты.
Классический демон - Основной процесс открывает соединение и форкает дочку. Дочка обрабатывает соединение и закрывает сокет, а основной процесс, после форканья - готов к приему нового соединения. И не надо мудрить с легкими и тяжелыми данными. Все данные надо пропускать через дочерние процессы. ИМХО, могу быть не прав

-~{}~ 01.12.08 20:57:

а зачем форкать процесс если можно не форкать (это риторический во
чтоб не ждать, если придет в этот момент следующий запрос...
 

Vict0r

Новичок
Настройка socket_select() не спасает в этом случае, потому как демон будет занят выводом данных, пока будут приходить и накапливаться новые соединения. Только после вывода данных, т.е. по истечении n-ного количества секунд, демон снова сможет обратиться к socket_select().

Вопрос в том, как грамотно реализовать взаимодействие с соединением от клиента, чтобы родительский процесс передал вывод данных дочернему и, грубо говоря, забыл об этом соединении и снова перешел бы к socket_select();
 

rotoZOOM

ACM maniac
Vict0r почитайте комментарии тут. Там есть пример такого сервера, немного доработать напильником и вперед.
 

Wicked

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


Vict0r
в том то и дело, что socket_select с неблокирующими сокетами позволяет работать в подобных сценариях:
- socket_select()
- приняли запрос на small
- отдали ответ
- socket_select()
- приняли 2 запроса на small
- отдали 2 ответа
- socket_select()
- приняли 2 запроса на small и 2 запрос на big
- отдали 2 small-ответа, отдали первый килобайт каждому из big'ов
- socket_select()
- первый big стал доступен для получения следующих данных
- отдали первому бигу второй килобайт данных
- socket_select()
- появился новый small-запрос, оба big'а стали доступны для получения следующих данных
- отдали small-ответ, отдали первому бигу третий килобайт данных, а второму второй
...

так работают любые мультиплексирующие сервера.

в частности, phpsocketdaemon, но это уже скорее http-сервер, хотя ядро у него вполне абстрактное. После небольшой доработки напильником вполне подойдет.
 

Vict0r

Новичок
rotoZOOM, спасибо. По-моему, это то, что надо. Буду разбираться.

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

MiksIr

miksir@home:~$
Не нужно на элементарные задачи рождать мультиплексор. Он сложнее и в реализации и в отладке. Если плотность запросов невысока и скорость ответа small достаточна, что бы все клиенты не строились в очередь - подход топикстартера весьма верен. Отдаем small основным процессом не создавая накладных на форке и форкаемся на тяжелом запросе.
С форком все просто - форкнутое детя наследует все данные и все сокеты родителя. После форка в родителе ты закрываешь клиентский сокет и возвращаешься к акцепту, в дите ты работаешь с клиентским сокетом отправляя туда данные и потом просто помираешь. В родителе так же делаешь сборщик мертвых детей... вот тут есть пример похожий
http://ru2.php.net/manual/en/function.socket-accept.php#80691
 

dr-sm

Новичок
не очень хорошо рождать процесс только с целью сразу же залипнуть в нем на ио.
мертворожденные дети какие-то.
 

Vict0r

Новичок
Вот, что получилось...

PHP:
define( "SERVER_IP", "0.0.0.0" );
define( "SERVER_PORT", 8888 );
define( "SCK_WRITE_PACKET_SIZE", 8192 );
define( "SCK_READ_PACKET_SIZE", 4096 );
define( "SCK_LISTEN_QUEUE", 20 );

function eval_data( $socket_id, $input_data )
{
	global $main_socket, $client_sockets, $is_parent, $server_runs;

	$client_request = explode( "\r\n", $input_data );
	$client_request = $client_request[0];

	$peer_host = '';
	$peer_port = '';

	socket_getpeername( $client_sockets[$socket_id], $peer_host, $peer_port );

	/*
	 * ЗАПРОС НА БОЛЬШОЙ ОБЪЕМ ДАННЫХ.
	 */
	if( $client_request == 'big' )
	{
		$pid = pcntl_fork();

		if( $pid == -1 )
		{
			make_log( "[*ERROR*] Fork failed!" );
			exit();
		}
		elseif( $pid )
		{
			@socket_close( $client_sockets[$socket_id] );
			$client_sockets[$socket_id] = null;
		}
		else
		{
			@socket_close( $main_socket );

			$is_parent = false;
			$server_runs = false;

			make_log(( $is_parent ? '<parent>' : '<child>' ) . " " . $peer_host . ":" . $peer_port . " requested BIG data..." );

			$data = "Here is your BIG data ))) Good bye.";
			if( !@socket_write( $client_sockets[$socket_id], $data ))
			{
				make_log( ( $is_parent ? '<parent>' : '<child>' ) . " [*ERROR*] Could not write to " . $socket_id . " ( " . get_socket_error( $client_sockets[$socket_id] ) . " )" );
			}
			else
			{
				make_log( ( $is_parent ? '<parent>' : '<child>' ) . " Sent [" . $data . "] to " . $peer_host . ":" . $peer_port . " and closed connection." );
			}

			@socket_close( $client_sockets[$socket_id] );
			$client_sockets[$socket_id] = null;
		}
	}

	/*
	 * ЗАПРОС НА МАЛЕНЬКИЙ ОБЪЕМ ДАННЫХ.
	 */
	elseif( $client_request == 'small' )
	{
		make_log(( $is_parent ? '<parent>' : '<child>' ) . " " . $peer_host . ":" . $peer_port . " requested SMALL data..." );

		$data = "Here is your SMALL data ))) Stay online ;-)";
		if( !@socket_write( $client_sockets[$socket_id], $data ))
		{
			make_log( ( $is_parent ? '<parent>' : '<child>' ) . " [*ERROR*] Could not write to " . $socket_id . " ( " . get_socket_error( $client_sockets[$socket_id] ) . " )" );
		}
		else
		{
			make_log( ( $is_parent ? '<parent>' : '<child>' ) . " Sent [" . $data . "] to " . $peer_host . ":" . $peer_port );
		}
	}

	/*
	 * НЕИЗВЕСТНЫЙ ЗАПРОС.
	 */
	else
	{
		make_log(( $is_parent ? '<parent>' : '<child>' ) . " " . $peer_host . ":" . $peer_port . " sent unknown request. Closing connection." );

		@socket_close( $client_sockets[$socket_id] );
		$client_sockets[$socket_id] = null;
	}
}

function read_from_socket( $socket_id )
{
	global $client_sockets, $read_end_char;

	$output_data = '';

	while( $temp = @socket_read( $client_sockets[$socket_id], 512 ))
	{
		$output_data .= $temp;
		$string_end = substr( $temp, - strlen( $read_end_char ));

		if( $string_end == $read_end_char )
		{
			break;
		}
	}

	return $output_data;
}

function accept_connection( &$socket_resource )
{
	global $client_sockets;

	for( $i = 0 ; $i <= sizeof( $client_sockets ); $i++ )
	{
		if( !isset( $client_sockets[$i] ))
		{
			$client_sockets[$i] = socket_accept( $socket_resource );
			socket_set_option( $client_sockets[$i], SOL_SOCKET, SO_REUSEADDR, 1 );

			return $i;
		}
	}
}

function get_socket_error( &$socket_id )
{
	return "msg: " . socket_strerror( socket_last_error( $socket_id )) . " / Code: " . socket_last_error( $socket_id );
}

function make_log( $message_text )
{
	echo $message_text . "\n";
	flush();
}

/*
 * ОБРАБОТКА СИГНАЛОВ.
 */
function sig_handler( $signal_type )
{
	global $server_runs;

	switch( $signal_type )
	{
		case SIGTERM:
		case SIGINT:
			$server_runs = false;
			break;

		case SIGCHLD:
			pcntl_waitpid( -1, $status );
			break;
	}
}

$_null = array();
$client_sockets = array();
$read_end_char = "\r\n";
$is_parent = true;

make_log( "----------------" );
make_log( "Test Server" );
make_log( "----------------" );

declare( ticks = 1 );

pcntl_signal( SIGTERM, 'sig_handler' );
pcntl_signal( SIGINT, 'sig_handler' );
pcntl_signal( SIGCHLD, 'sig_handler' );

if(( $main_socket = @socket_create( AF_INET, SOCK_STREAM, 0 )) < 0 )
{
	make_log( "[*ERROR*] Couldn't create socket!" );
	exit();
}

socket_set_option( $main_socket, SOL_SOCKET, SO_REUSEADDR, 1 );

$send_buffer = socket_get_option( $main_socket, SOL_SOCKET, SO_SNDBUF );

if( $send_buffer < ( SCK_WRITE_PACKET_SIZE * 32 ))
{
	socket_set_option( $main_socket, SOL_SOCKET, SO_SNDBUF, SCK_WRITE_PACKET_SIZE * 32 );
}

$receive_buffer = socket_get_option( $main_socket, SOL_SOCKET, SO_RCVBUF );

if( $receive_buffer < ( SCK_READ_PACKET_SIZE * 32 ))
{
	socket_set_option( $main_socket, SOL_SOCKET, SO_RCVBUF, SCK_READ_PACKET_SIZE * 32 );
}

if( !@socket_bind( $main_socket, SERVER_IP, SERVER_PORT ))
{
	@socket_close( $main_socket );
	make_log( "[*ERROR*] Could not bind socket to " . SERVER_IP . ":" . SERVER_PORT . " ( ".get_socket_error( $main_socket )." )" );
	exit();
}

$log_message = "Listening on " . SERVER_IP . ":" . SERVER_PORT . "...";

if( !@socket_listen( $main_socket, SCK_LISTEN_QUEUE ))
{
	make_log( $log_message . " FAILED" );
	exit();
}
else
{
	make_log( $log_message . " OK" );
}

set_time_limit( 0 );
$server_runs = true;

while( $server_runs )
{
	$test_sockets = array();
	array_push( $test_sockets, $main_socket );

	for( $i = 0; $i < sizeof( $client_sockets ); $i++ )
	{
		if( isset( $client_sockets[$i] ))
		{
			array_push( $test_sockets, $client_sockets[$i] );
		}
	}

	$ready = @socket_select( $test_sockets, $_null, $_null, null );

	if( $ready === false )
	{
		make_log( "[*ERROR*] Socket_select failed! (" . socket_strerror( socket_last_error()) . ")" );
	}

	if( in_array( $main_socket, $test_sockets ))
	{
		$temp = accept_connection( $main_socket );

		if( --$ready <= 0 )
			continue;
	}

	for( $i = 0; $i < sizeof( $client_sockets ); $i++ )
	{
		if( !isset( $client_sockets[$i] )) continue;

		if( in_array( $client_sockets[$i], $test_sockets ))
		{
			$output_data = read_from_socket( $i );

			if( !$output_data )
			{
				@socket_close( $client_sockets[$i] );
				$client_sockets[$i] = null;
			}
			else eval_data( $i, $output_data );
		}
	}
}

if( $is_parent )
{
	for( $i = 0; $i < sizeof( $client_sockets ); $i++ )
	{
		@socket_close( $client_sockets[$i] );
	}

	@socket_close( $main_socket );

	make_log( "*** SERVER STOPPED ***" );
}

exit;
Запускаю демона.

----------------
Test Server
----------------
Listening on 0.0.0.0:8888... OK


Соединяюсь с ним через telnet-терминал и передаю слово "small":

# telnet localhost 8888
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
small
Here is your SMALL data ))) Stay online ;-)


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

<parent> 127.0.0.1:40227 requested SMALL data...
<parent> Sent [Here is your SMALL data ))) Stay online ;-)] to 127.0.0.1:40227


Соединяюсь с демоном с другого telnet-терминала и передаю слово "big":

# telnet localhost 8888
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
big
Here is your BIG data ))) Good bye.Connection closed by foreign host.


при этом соединение закрывается, все, вроде бы, нормально. Но в логах видим вот что:

<child> 127.0.0.1:40257 requested BIG data...
<child> Sent [Here is your BIG data ))) Good bye.] to 127.0.0.1:40257 and closed connection.
[*ERROR*] Socket_select failed! (Interrupted system call)


Теперь при попытке послать какие-либо данные в первое соединение, которое, как мы помним, осталось открытым, демон не отвечает. Продолжается это до тех пор, пока демон не получит новое соединение. После чего любые запросы, кроме слова "big", обрабатываются нормально. Но если послать "big", то история повторится...

Подскажите, пожалуйста, что я упустил? Я понимаю, что ответ в socket_select(), на котором выполнение родительского процесса "зависает", но не могу понять, из-за чего выдается ошибка "Interrupted system call", ведь, вроде бы, все делаю правильно при fork-ании.

С уважением.
 

dr-sm

Новичок
Interrupted system call
походу
это когда у тебя главный процесс висит в select syscall'е
и приходит сигнал в sig_handler
имхо нужно просто проверять код ошибки E_INTR и рестартовать селект
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
1. А вы уверены, что socket_close в родителе не закроет сокет вообще? Помню, файл и соединение с базой закрываются, и чайлд юзать его уже не может.
Я максимум unset переменной в родителе делал.

2. после if( $pid == -1 ) закрывать сокет как-раз стоит :)

3. if( $pid ) ... else
$is_parent = false;

Чет-у меня в памяти осталось, что чайлда отличают по изменившемуся пиду, а не по его отсутствию.

-~{}~ 07.12.08 17:39:

таки да, по мануалу в чайлде возвращается 0 - я ошибся
 

MiksIr

miksir@home:~$
Я вот чего не понял...
зачем реализуя мультиплексор делать форк?
 
Сверху