Прием потока данных через TCP сокет на PHP?

На nix сервере постоянно запущен php скрипт, принимающий данные через Интернет от удаленных датчиков по GPRS/EDGE каждые несколько секунд.



В Сети нашлось множество примеров работы с сокетами в php однако у всех них общий недостаток — через несколько десятков минут непрерывной работы скрипт «зависает» и сокеты порожденные им висят в статусе FIN_WAIT_1.



Я временно решил данную проблему установив режим сокета в неблокирующий режим и включив SO_LINGER ('l_onoff'=>1, 'l_linger'=>0), т.е. грубо закрываю сокет не дожидаясь ответа после получения данных.



Однако данная мера вызывает реконнект дорогостоящего GPRS соединения ибо операторы округляют трафик по 100кб обычно, т.е. факт соединения уже 100кб считай утекло. Хотелось бы увидеть реально рабочий пример как можно НЕ разрывая соединения стабильно принимать данные от датчиков и при этом не вызывать «зависание» сокетов и самого скрипта.



Ниже привожу часть кода с учетом моих поправок по приему данных на PHP:



$socket = socket_create_listen($port, SOMAXCONN) ;
socket_set_nonblock($socket);
$arrOpt = array('l_onoff'=>1, 'l_linger'=>0);

while(1) {
	usleep(100000);
	if((time()-$time) >= 60) { $time = time(); UpdatePID($pid); }
	$client = @socket_accept($socket);
	if(!$client) continue;
	socket_set_option($client, SOL_SOCKET, SO_LINGER, $arrOpt);
	$data = socket_read($client, 4096);
	$data = trim($data);
	Save2DB($data);
	socket_close($client);
	if(strtolower($input)=="exit") break; }

socket_close($socket);




UpdatePID — обновляет pid файл, необходимый для предотвращения повторного запуска скрипта.

Save2DB — парсит и пишет данные в базу.



С использованием socket_set_option(..SO_KEEPALIVE...) никто не работал?
  • Вопрос задан
  • 21153 просмотра
Решения вопроса 1
@galaxy
Вы что-то не то делаете с сокетами.
FIN_WAIT_1 говорит о том, что ваш сервер соединение закрыл со своей стороны и ждет закрытия со стороны клиента. Имхо, происходит такое потому, что, сделав socket_read(), вы делаете socket_accept() следующему соединению, переменная $client (предыдущий сокет) попадает в сборщик мусора, который пытается корректно завершить соединение. Нельзя так выбрасывать заакцепченный сокет, надо продолжать пытаться читать из него данные, одновременно проверяя, нет ли новых запросов на соединение. Можно организовать это и через sleep'ы, но классический подход состоит в использовании socket_select(). Следуя вашему подходу, можно написать что-то в этом духе:
while(1) {
  usleep(100000);
  if((time()-$time) >= 60) { $time = time(); UpdatePID($pid); }
  $client = @socket_accept($socket);
  if($client) {
    $clients[] = $client;
  }
  foreach($clients as $i => $c) {
    $data = socket_read($client, 4096);
    if ($data === false) { // error or closed connection
      socket_close($c);
      unset($clients[$i]);
    } elseif (strlen($data)) {
      Save2DB(trim($data));
    }
  }

  ...
Ответ написан
Пригласить эксперта
Ответы на вопрос 3
zizop
@zizop
Думаю вам стоит попробовать работу с неблокирующими сокетами через libevent.
<?php
$socket = stream_socket_server ('tcp://0.0.0.0:2000', $errno, $errstr);
stream_set_blocking($socket, 0);
$base = event_base_new();
$event = event_new();
event_set($event, $socket, EV_READ | EV_PERSIST, 'ev_accept', $base);
event_base_set($event, $base);
event_add($event);
event_base_loop($base);

$GLOBALS['connections'] = array();
$GLOBALS['buffers'] = array();

function ev_accept($socket, $flag, $base) {
static $id = 0;

$connection = stream_socket_accept($socket);
stream_set_blocking($connection, 0);

$id += 1;

$buffer = event_buffer_new($connection, 'ev_read', NULL, 'ev_error', $id);
event_buffer_base_set($buffer, $base);
event_buffer_timeout_set($buffer, 30, 30);
event_buffer_watermark_set($buffer, EV_READ, 0, 0xffffff);
event_buffer_priority_set($buffer, 10);
event_buffer_enable($buffer, EV_READ | EV_PERSIST);

// we need to save both buffer and connection outside
$GLOBALS['connections'][$id] = $connection;
$GLOBALS['buffers'][$id] = $buffer;
}

function ev_error($buffer, $error, $id) {
event_buffer_disable($GLOBALS['buffers'][$id], EV_READ | EV_WRITE);
event_buffer_free($GLOBALS['buffers'][$id]);
fclose($GLOBALS['connections'][$id]);
unset($GLOBALS['buffers'][$id], $GLOBALS['connections'][$id]);
}

function ev_read($buffer, $id) {
while ($read = event_buffer_read($buffer, 256)) {
var_dump($read);
}
}
?>

См. ru2.php.net/manual/en/book.libevent.php и ru2.php.net/manual/en/libevent.examples.php
Ответ написан
Ramzeska
@Ramzeska
Попробуйте переписать на inetd — на нем все получается в разы проще и компактнее.

Регистрируете свой порт в /etc/services:
myrpc 8000/tcp
Добавляете свой демон в /etc/inetd.conf:
myrpc stream tcp nowait ramzes /home/ramzes/tmp/myrpc.php myrpc.php

#!/usr/local/bin/php
<?

$f=fopen("php://stdin","r");

$data=fread($f,1024);

Save2DB($data);

fclose($f);
Ответ написан
@RomanGPS
Получить поток через inetd действительно довольно просто, правда несколько дней длилось прежде чем провайдер настроит это в VPS. у меня еще трекер который шлет все в бинарном формате и просит ответ. Вот с ответом пока что то не получается ответить

$f=fopen("php://stdin","r");
$data=fread($f,1024);
$array = unpack("c*", $data);
//составляем ответ из полученной строки
$talkback=pack("c*",0x78,0x78,0x05,0x01,$array[13],$array[14],0xD9,0xDC,0x0D,0x0A);
echo $talkback;
//далье после ответа должен начать присылать данные о кооординатах но он упорно шлет первую строку логина, видимо не получает ответа через echo
fclose($f);
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы