S
S
SSar2011-01-05 15:25:10
PHP
SSar, 2011-01-05 15:25:10

Receiving data stream over TCP socket in PHP?

A php script is constantly running on the nix server, which receives data via the Internet from remote sensors via GPRS / EDGE every few seconds.

There were many examples of working with sockets in php on the Web, however, all of them have a common drawback - after several tens of minutes of continuous operation, the script “hangs” and the sockets generated by it hang in the FIN_WAIT_1 status.

I temporarily solved this problem by setting the socket mode to non-blocking mode and enabling SO_LINGER ('l_onoff'=>1, 'l_linger'=>0), i.e. I roughly close the socket without waiting for a response after receiving data.

However, this measure causes a reconnection of an expensive GPRS connection, because operators usually round off traffic by 100 kb, i.e. the fact of the connection is already 100kb considered leaked. I would like to see a really working example of how you can stably receive data from sensors without breaking the connection and at the same time not cause the sockets and the script itself to hang.

Below is a part of the code, taking into account my corrections for receiving data in 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 - Updates the pid file needed to prevent the script from running again.
Save2DB - parses and writes data to the database.

Has anyone worked with socket_set_option(..SO_KEEPALIVE...)?

Answer the question

In order to leave comments, you need to log in

4 answer(s)
G
galaxy, 2011-01-07
@SSar

You are doing something wrong with sockets.
FIN_WAIT_1 indicates that your server has closed the connection on its side and is waiting for the client to close the connection. IMHO, this happens because by doing socket_read() you are doing socket_accept() to the next connection, the $client variable (previous socket) gets caught by the garbage collector, which tries to terminate the connection gracefully. You can't throw away an accepted socket like that, you have to keep trying to read data from it, while checking for new connection requests. It is possible to organize this via sleeps, but the classic approach is to use socket_select() . Following your approach, one could write something along the lines of:

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));
    }
  }

  ...

Z
zizop, 2011-01-06
@zizop

I think you should try working with non-blocking sockets through 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);
}
}
?>

See ru2.php.net/manual/en/book.libevent.php and ru2.php.net/manual/en/libevent.examples.php

R
Ramzeska, 2011-01-08
@Ramzeska

Try to rewrite on inetd - everything is much simpler and more compact on it.
Register your port in /etc/services:
myrpc 8000/tcp
Add your daemon to /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);

R
RomanGPS, 2017-08-28
@RomanGPS

Getting a stream through inetd is really quite simple, although it took several days before the provider configured this in the VPS. I also have a tracker that sends everything in binary format and asks for an answer. So far, with the answer, it's not possible to answer
$f=fopen("php://stdin","r");
$data=fread($f,1024);
$array = unpack("c*", $data);
//compose the answer from the received string
$talkback=pack("c*",0x78,0x78,0x05,0x01,$array[13],$array[14],0xD9,0xDC,0x0D,0x0A);
echo $talkback;
//further after the response, it should start sending data about the coordinates, but it persistently sends the first login line, apparently does not receive a response through echo
fclose($f);

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question