Как избежать подключения TCP застрял PHP Socket (FIN_WAIT1) - PullRequest
1 голос
/ 11 июля 2019

У меня есть команда Symfony, которая запускается при запуске с супервизором на компьютере с Debian 9.Он открывает сокет прослушивателя TCP для приема сообщений от многих устройств через порт.

Через некоторое время работы (между 15 - 20 часами) он перестает работать и блокирует порт TCP.

Эта проблема возникает на компьютере Debian 9, работающем с PHP 7.3 и Apache2.

Этот код открывает сокет TCP:

protected function execute(InputInterface $input, OutputInterface $output)
{
    try {
        $this->io = new SymfonyStyle($input, $output);
        $this->logger->info('Start TCP socket: Setup websocket server for detections on port 8086...');

        $now = new \DateTime();
        $this->io->title('Start server ' . $now->format('d-m-Y G:i:s') . '...');

        $server = socket_create_listen(8086);
        socket_getsockname($server, $addr, $port);
        if (!$server) {
            $message = 'Start TCP socket: Ko. Could not create socket.';
            $this->io->error($message);
            $this->logger->critical($message);
            die($message);
        } else {
            $message = 'Start TCP socket: Ok. TCP socket opened on: ' . $addr . ':' . $port . '.';
            $this->io->success($message);
            $this->logger->info($message);

            while ($c = socket_accept($server)) {
                socket_getpeername($c, $raddr, $rport);
                $this->io->writeln("Received Connection from $raddr:$rport\n");

                $data = '';
                while ($bytes = socket_recv($c, $r_data, 128, MSG_WAITALL)) {
                    $data .= $r_data;
                }

                //Process data here

                socket_close($c);

                $message = 'Finish processing data. Total Data Received: ' . strlen($data) . PHP_EOL;
                $this->io->writeln($message);
                $this->logger->info($message);
            }
        }
        fclose($server);
    } catch (Exception $exception) {
        $message = 'Start TCP socket: Ko. Exception catched. Error detail: ' . $exception->getMessage();
        $this->logger->critical($message);
        $this->io->error($message);
    }
}

Когда сокет прекращает соединение, я пишу этоКоманда в консоли:

sudo netstat -np | grep :8086

Показывает этот вывод:

tcp 0 1 172.25.1.14:8086 88.0.111.77:47794 FIN_WAIT1 -

Как можно избежатьэта проблема и попытаться перезапустить службу или не заблокировать порт?

Спасибо.

1 Ответ

0 голосов
/ 11 июля 2019

Для конкретной проблемы нормальная работа завершения TCP (close()) входит в состояние FIN_WAIT_1, что может привести к тому, что открытый сокет останется открытым, пока не получит FIN.Чтобы решить эту проблему, вы можете указать сокету не ждать, установив SO_LINGER в 0.Однако это считается "плохой практикой".Для получения более подробной информации см .: Параметр TCP SO_LINGER (ноль) - когда это необходимо

Если вы используете PHP в качестве клиента для отправки запросов в эту Команду, обновите свой вопрос следующим образом:он может неправильно завершать запрос (ы).

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

Вы должны добавить finally к вашему try/catch для обработки обычного завершения или исключения, которое может закрывать открытые сокеты.Начиная с PHP 7.0 вы должны ловить \Throwable, а не только \Exception.Например, использование intdiv(1, 0) или 1 << -1 не сможет быть перехвачено вашим кодом.

При условии, что супервизор правильно контролирует процесс.

protected function execute(InputInterface $input, OutputInterface $output)
{
    try {
        $this->io = new SymfonyStyle($input, $output);
        $this->logger->info('Start TCP socket: Setup websocket server for detections on port 8086...');        

        $now = new \DateTime();
        $this->io->title('Start server ' . $now->format('d-m-Y G:i:s') . '...');

        if (!$localSocket = socket_create_listen(8086)) {
            throw new \RuntimeException('Could not create socket.');
        }
        //force PHP to close the socket (do not linger waiting for FIN)
        socket_set_option($localSocket, SOL_SOCKET, SO_LINGER, [
            'l_linger' => 0,
            'l_onoff' => 1,
        ]);
        if (!socket_getsockname($localSocket, $addr, $port)) {
            throw new \RuntimeException('Unable to retrieve local socket.');
        }

        $message = sprintf('Start TCP socket: Ok. TCP socket opened on: %s:%s.', $addr, $port);
        $this->io->success($message . PHP_EOL);
        $this->logger->info($message);
        $listening = true;
        while ($listening) {
            if (!$remoteSocket = socket_accept($localSocket)) {
                throw new \RuntimeException('Unable to accept incoming connections');
            }
            if (!socket_getpeername($remoteSocket, $raddr, $rport)) {
                throw new \RuntimeException('Unable to retrieve remote socket');
            }
            $this->io->writeln(sprintf('Received Connection from %s:%s%s', $raddr, $rport, PHP_EOL));

            $data = '';
            $bytesRec = 0;
            while ($bytes = socket_recv($remoteSocket, $r_data, 128, MSG_WAITALL)) {
                $data .= $r_data;
                $bytesRec += $bytes;
            }
            //force PHP to close the socket (do not linger waiting for FIN)
            socket_set_option($remoteSocket, SOL_SOCKET, SO_LINGER, [
                'l_linger' => 0, 
                'l_onoff' => 1
            ]);
            //clear memory of remoteSocket resource before processing the data
            socket_close($remoteSocket);
            $remoteSocket  = null;
            unset($remoteSocket);

            //Method Call to process data here...

            $message = sprintf('Finish processing data. Total Data Received: %s %s', $bytesRec, PHP_EOL);
            $this->io->writeln($message);
            $this->logger->info($message);

             if ($condition = false) {
                //add a condition to terminate listening, such as $i++ >= 1000
                $listening = false;
             }

            //force PHP to take a break
            usleep(100);
        }
    } catch (\Throwable $e) {
        $message = sprintf('Start TCP socket: Ko. Exception detail: %s', 
            $e->getMessage());
        $this->logger->critical($message);
        $this->io->error($message);
    } finally {
        //ensure the socket resources are closed
        if (isset($remoteSocket) && is_resource($remoteSocket)) {
            //force PHP to close the socket (do not linger waiting for FIN)
            socket_set_option($remoteSocket, SOL_SOCKET, SO_LINGER, [
                'l_linger' => 0, 
                'l_onoff' => 1
            ]);
            socket_close($remoteSocket);
            $remoteSocket = null;
            unset($remoteSocket);
        }
        if (isset($localSocket) && is_resource($localSocket)) {
            socket_close($localSocket);
            $localSocket = null;
            unset($localSocket);
        }
    }
}

Как примечание;использование службы Doctrine в конечном итоге приведет к потере соединения с базой данных, когда ваш сервер БД превысит исходное соединение, созданное Symfony.Это приведет к неожиданному завершению работы сценария при попытке выполнить запрос к БД.

Вам необходимо будет закрыть соединение при каждом вызове службы базы данных или, если она введена в вашу команду, немедленно закройтеподключение во время execute.

class YourCommand
{
    private $em;

    public function __construct(EntityManagerInterface $em) 
    {
        $this->em = $em; //or $this->container->get('doctrine.orm.entity_manager')
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $this->em->getConnection()->close(); 

        //...

        while ($listening) {

            //...

            //Method Call to process data here...
            $this->em->getConnection()->connect();
            //... execute query
            $this->em->getConnection()->close();

            //...
        }
    } 
}

Стоит также отметить, что PHP не предназначен для запуска в качестве долго запущенного процесса демона и имеет известных проблем с этим ,такие как утечки памяти.Настоятельно рекомендуется найти другой подходящий подход, например NodeJS, для посредничества запросов TCP от удаленных подключений к PHP (как это делают Apache и NGINX).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...