PHP сокеты. Проблема с таймаутом

Nikita92

Новичок
Всем доброго времени суток. Пожалуйста, помогите разобраться с проблемой.
Я написал небольшой чат с использованием WebSockets и самописным сервером на php без использования phpDaemon, Libevent и прочих подобных вещей.
Всё работает как надо, кроме одной детали: остаётся много зависших коннектов.
При закрытии браузера или вкладки клиент отключается правильно, но если у пользователя, например, пропадает
соединение с интернетом, то коннект остаётся висеть на сервере ещё ровно 20 минут.
Никак не могу понять в чем проблема. Возможно в функции stream_socket_accept? Я перепробовал все возможные значения таймаута, но ничего не меняется.
Может есть какая-то настройка PHP, о которой я не знаю?

Вот кусок кода сервера:

PHP:
$socket = stream_socket_server("tcp://0.0.0.0:8080", $errno, $errstr);

$this->stop_server = false;
$this->connects = array();
$this->sites = array();

while($this->stop_server == false){

    //Формируем массив прослушиваемых сокетов
    $read = $this->connects;
    $read[]= $socket;
    $write = $except = null;

    if(!stream_select($read, $write, $except, null)){
        break;
    }

    if(in_array($socket, $read)){
        if(($connect = stream_socket_accept($socket, -1)) && $info = $this->handshake($connect)){
            $this->connects[] = $connect;
            $this->on_open($connect, $info);
        }
        unset($read[array_search($socket, $read)]);
    }

    //Обрабатываем все соединения
    foreach($read as $connect){
   
        $data = fread($connect, 1024);
       
        $disconnect = false;
       
        if(!$data || strlen($data) === 0){
            $disconnect = true;
        } else {
            $data = $this->decode($data);
            if($data['type'] == 'close'){
                $disconnect = true;
            }   
        }

        if($disconnect == true){
            //Соединение было закрыто
            $this->disconnect_client($connect);
            continue;
        }                   

    }

}
 

Nikita92

Новичок
Не хочется ставить и разбираться с крупными библиотеками, если требуется всего пара строчек кода. Все уже готово и работает как надо. Остается только одна проблема - таймаут. Функцию stream_set_timeout пробовал и она не работает, хоть и возвращает true. Неактивное соединение в любом случае рвется ровно через 20 минут.
 

ksnk

прохожий
Функцию stream_set_timeout пробовал и она не работает, хоть и возвращает true. Неактивное соединение в любом случае рвется ровно через 20 минут.
Когда время работы потока истекает, ключ 'timed_out' массива, возвращаемого функцией stream_get_meta_data(), устанавливается в значение TRUE, хотя ошибка или предупреждение не генерируется.
Именно это проверялось?
 

Nikita92

Новичок
Именно это проверялось?
Проблема в том, что stream_select ждет изменения в потоках и пока этого не произойдет - дальнейший код не выполняется. Как в этом случае проверять параметр timed_out? Как вариант, можно добавить в неё необязательный параметр таймаута, но тогда загрузка процессора сильно возрастает. Думал может есть более оптимальное решение
 

WMix

герр M:)ller
Партнер клуба
Не хочется ставить и разбираться с крупными библиотеками, если требуется всего пара строчек кода.
еслиб это была пара сточек, и еслиб ты не читал, яб тебя понял, но навалял ты много! а взяв бы хорошую библиотечку, написал бы 1 сточку в композере да имплементировал бы пару методов (исходя из тобою описанного это будут пустые методы) то былобы 5-10 строк чистой бизнеслогики
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
Функцию stream_set_timeout пробовал и она не работает.
Неактивное соединение в любом случае рвется ровно через 20 минут.
не верю, покажи минимальный скрипт, который воспроизводит проблему - на одном соединении и без собственных методов, описания которых мы не видим
 

Nikita92

Новичок
не верю, покажи минимальный скрипт, который воспроизводит проблему - на одном соединении и без собственных методов, описания которых мы не видим
Нет больше никаких собственных методов. Я их временно убрал для теста. Сейчас есть всего 2 файла: этот и обычный html файл, в котором всего 1 строчка var socket = new WebSocket("...");

Открываю этот файл на телефоне - появляется коннект, закрываю вкладку - коннект пропадает. Потом открываю обратно и отключаю вайфай. Коннект висит ровно 20 минут и отключается. Специально засекал ровно 20 минут.
 

Nikita92

Новичок
talk is cheap, show me the code (C) Linus Torvalds
Полный листинг программы:

PHP:
<?php

set_time_limit(0);
ignore_user_abort(true);
ob_implicit_flush();

class ws_socket_server{
   
   private $stop_server = false;
   private $connects = array();
   
   public function index(){
     
     $socket = stream_socket_server("tcp://0.0.0.0:8080", $errno, $errstr);
     
     $this->stop_server = false;
     $this->connects = array();

     while($this->stop_server == false){

       $read = $this->connects;
       $read[]= $socket;
       $write = $except = null;

       if(!stream_select($read, $write, $except, null)){
         break;
       }

       if(in_array($socket, $read))
         {
           if(($connect = stream_socket_accept($socket, -1)) && $info = $this->handshake($connect))
             {
               $this->connects[] = $connect;
               $this->on_open($connect, $info);
             }
           unset($read[array_search($socket, $read)]);
         }

       foreach($read as $connect)
         {
           $data = fread($connect, 1024);
           $disconnect = false;
           if(!$data || strlen($data) === 0){
             $disconnect = true;
           } else {
             $data = $this->decode($data);
             if($data['type'] == 'close'){
               $disconnect = true;
             }   
           }
           
           if($disconnect == true){
             //Соединение было закрыто
             $this->disconnect_client($connect);
             continue;
           }           

           $this->on_message($connect, $data);
           
         }
         

     }

     fclose($socket);
     echo "Сервер остановлен".PHP_EOL;
     exit;
     
   }
   
   //Пользовательские сценарии:
   private function on_open($connect, $info){
     echo "Клиент <font color=\"green\">подключился</font><br>".PHP_EOL;
   }
   private function on_close($connect) {
     echo "<font color=\"red\">Клиент отключился</font><br>".PHP_EOL;
   }
   private function on_message($connect, $data){

     $response = json_decode($data['payload']);

     if($response){

       switch($response->f){

         case 'exit':
           $this->stop_server = true;
         break;
         
         case 'info':
           fwrite($connect, $this->encode('Подключено клиентов: '.count($this->connects)));
         break;

       }//switch

     }
       
   }
   
   private function disconnect_client($connect){
     fclose($connect);
     unset($this->connects[array_search($connect, $this->connects)]);
     $this->on_close($connect);
   }


   private function handshake($connect){
     $info = array();

     $line = fgets($connect);
     $header = explode(' ', $line);
     
     if(!isset($header[0]) || !isset($header[1])){
       
       var_dump($info);
       $this->disconnect_client($connect);
       return false;
       
     }
     
     $info['method'] = $header[0];
     $info['uri'] = $header[1];

     //считываем заголовки из соединения
     while($line = rtrim(fgets($connect))) {
       if (preg_match('/\A(\S+): (.*)\z/', $line, $matches)) {
         $info[$matches[1]] = $matches[2];
       } else {
         break;
       }
     }

     $address = explode(':', stream_socket_get_name($connect, true)); //получаем адрес клиента
     $info['ip'] = $address[0];
     $info['port'] = $address[1];

     if(empty($info['Sec-WebSocket-Key'])) {
       return false;
     }

     //отправляем заголовок согласно протоколу вебсокета
     $SecWebSocketAccept = base64_encode(pack('H*', sha1($info['Sec-WebSocket-Key'] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
     $upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" .
       "Upgrade: websocket\r\n" .
       "Connection: Upgrade\r\n" .
       "Sec-WebSocket-Accept:$SecWebSocketAccept\r\n\r\n";
     fwrite($connect, $upgrade);

     return $info;
   }
   private function encode($payload, $type = 'text', $masked = false){
     $frameHead = array();
     $payloadLength = strlen($payload);

     switch ($type) {
       case 'text':
         // first byte indicates FIN, Text-Frame (10000001):
         $frameHead[0] = 129;
         break;

       case 'close':
         // first byte indicates FIN, Close Frame(10001000):
         $frameHead[0] = 136;
         break;

       case 'ping':
         // first byte indicates FIN, Ping frame (10001001):
         $frameHead[0] = 137;
         break;

       case 'pong':
         // first byte indicates FIN, Pong frame (10001010):
         $frameHead[0] = 138;
         break;
     }

     // set mask and payload length (using 1, 3 or 9 bytes)
     if ($payloadLength > 65535) {
       $payloadLengthBin = str_split(sprintf('%064b', $payloadLength), 8);
       $frameHead[1] = ($masked === true) ? 255 : 127;
       for ($i = 0; $i < 8; $i++) {
         $frameHead[$i + 2] = bindec($payloadLengthBin[$i]);
       }
       // most significant bit MUST be 0
       if ($frameHead[2] > 127) {
         return array('type' => '', 'payload' => '', 'error' => 'frame too large (1004)');
       }
     } elseif ($payloadLength > 125) {
       $payloadLengthBin = str_split(sprintf('%016b', $payloadLength), 8);
       $frameHead[1] = ($masked === true) ? 254 : 126;
       $frameHead[2] = bindec($payloadLengthBin[0]);
       $frameHead[3] = bindec($payloadLengthBin[1]);
     } else {
       $frameHead[1] = ($masked === true) ? $payloadLength + 128 : $payloadLength;
     }

     // convert frame-head to string:
     foreach (array_keys($frameHead) as $i) {
       $frameHead[$i] = chr($frameHead[$i]);
     }
     if ($masked === true) {
       // generate a random mask:
       $mask = array();
       for ($i = 0; $i < 4; $i++) {
         $mask[$i] = chr(rand(0, 255));
       }

       $frameHead = array_merge($frameHead, $mask);
     }
     $frame = implode('', $frameHead);

     // append payload to frame:
     for ($i = 0; $i < $payloadLength; $i++) {
       $frame .= ($masked === true) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i];
     }

     return $frame;
   }
   private function decode($data){
     $unmaskedPayload = '';
     $decodedData = array();

     // estimate frame type:
     $firstByteBinary = sprintf('%08b', ord($data[0]));
     $secondByteBinary = sprintf('%08b', ord($data[1]));
     $opcode = bindec(substr($firstByteBinary, 4, 4));
     $isMasked = ($secondByteBinary[0] == '1') ? true : false;
     $payloadLength = ord($data[1]) & 127;

     // unmasked frame is received:
     if(!$isMasked){
       return array('type' => '', 'payload' => '', 'error' => 'protocol error (1002)');
     }

     switch($opcode){

       //text frame:
       case 1:
         $decodedData['type'] = 'text';
       break;

       case 2:
         $decodedData['type'] = 'binary';
       break;

       //connection close frame:
       case 8:
         $decodedData['type'] = 'close';
       break;

       // ping frame:
       case 9:
         $decodedData['type'] = 'ping';
       break;

       // pong frame:
       case 10:
         $decodedData['type'] = 'pong';
       break;

       default:
         return array('type' => '', 'payload' => '', 'error' => 'unknown opcode (1003)');
       break;

     }

     if($payloadLength === 126){
       $mask = substr($data, 4, 4);
       $payloadOffset = 8;
       $dataLength = bindec(sprintf('%08b', ord($data[2])).sprintf('%08b', ord($data[3]))) + $payloadOffset;
     } elseif ($payloadLength === 127) {
       $mask = substr($data, 10, 4);
       $payloadOffset = 14;
       $tmp = '';
       for ($i = 0; $i < 8; $i++) {
         $tmp .= sprintf('%08b', ord($data[$i + 2]));
       }
       $dataLength = bindec($tmp) + $payloadOffset;
       unset($tmp);
     } else {
       $mask = substr($data, 2, 4);
       $payloadOffset = 6;
       $dataLength = $payloadLength + $payloadOffset;
     }

     /**
      * We have to check for large frames here. socket_recv cuts at 1024 bytes
      * so if websocket-frame is > 1024 bytes we have to wait until whole
      * data is transferd.
      */
     if(strlen($data) < $dataLength){
       return false;
     }

     if ($isMasked) {
       for ($i = $payloadOffset; $i < $dataLength; $i++) {
         $j = $i - $payloadOffset;
         if (isset($data[$i])) {
           $unmaskedPayload .= $data[$i] ^ $mask[$j % 4];
         }
       }
       $decodedData['payload'] = $unmaskedPayload;
     } else {
       $payloadOffset = $payloadOffset - 4;
       $decodedData['payload'] = substr($data, $payloadOffset);
     }

     return $decodedData;
   }

}

$server = new ws_socket_server();
$server->index();

?>
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
я написал: для одного соединения без твоих классов
этот код какой угодно, только не минимальный, и никому не нужен.
дебажить и локализовывать проблему за тебя я не буду
если не собираешься делать что говорят - иди куда-нибудь в другое место
 

Nikita92

Новичок
В первом посте всего один цикл, в котором сокет ждет изменений и коннект добавляется в массив. Куда уж минимальнее? Не нравится, хорошо. Показал полный код - тоже не нравится. Тогда вопрос: зачем лезешь в эту тему, если не собираешься отвечать нормально?
 

Nikita92

Новичок
Попробуйте SO_KEEPALIVE
Судя по всему этот параметр работает только с socket_create, а у меня stream_socket_server и устанавливается через функцию socket_set_option().

Идея хорошая, но тогда придётся отказаться от потоков. Наверное так и сделаю. Спасибо за совет!
 

MiksIr

miksir@home:~$
Еще вариант просто писать что-то во все сокеты, типа пустых сообщений. Тогда select, конечно, нужно будет сделать с таймаутом.
Правда, попытки ретрансмита такого сообщения "в пустоту" тоже может занять прилично времени.
socket_set_option скорее всего нужно будет ставить на каждый новый сокет (новое соединение)
 

Nikita92

Новичок
Еще вариант просто писать что-то во все сокеты, типа пустых сообщений.
Надо будет попробовать.

А ещё вот нашел на форуме примерно то, что надо. Но опять же тут socket_create:
http://phpclub.ru/talk/threads/Сокет-демон-чтение-с-таймаутом.39902/#post-340539

Кстати и SO_KEEPALIVE сюда тоже можно будет впихнуть.
 
Сверху