PHP脚本在浏览器连接关闭时不会退出

PHP script doesn't quit when browser connection is closed

提问人:NickSoft 提问时间:10/30/2023 最后编辑:NickSoft 更新时间:11/8/2023 访问量:40

问:

注意:这不是我使用 php-fpm + apache mod_fcgi + 代理(见下文)25363635问题的重复connection_aborted() 和 ignore_user_abort() 在我的情况下不起作用。TLDR 是如何使它们与我的设置 - fcgi+proxy 一起工作。

我正在使用服务器发送的事件制作日志代理。我正在使用这些标头:

header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: close');

然后我收到套接字的日志行,并像这样输出它们:

foreach($lines as $line) {
  echo "data: $line\n\n";
}
flush();

问题是,当浏览器关闭时(也尝试使用curl),php脚本继续运行。这会占用内存,并且还会阻止打开新的服务器套接字来接收日志。 我在 apache 2.4 中使用 php-fpm (8.1) 和 fcgi。 Apache 配置如下所示:

  <FilesMatch "\.php$">
     SetHandler "proxy:unix:/usr/local/php81/var/run/website.sock|fcgi://localhost/"
  </FilesMatch>
  <Proxy "fcgi://localhost/">
    ProxySet timeout=600
  </Proxy>

是否可以在浏览器关闭连接时使 php 脚本退出,或者找到另一种(相对简单的)方法将日志实时转发到浏览器。我不想编写 Web 套接字服务器或安装其他软件,除非真的没有其他方法。

此外,我也喜欢出于其他目的进行长轮询的想法。但是,如果脚本在后台一直挂起,不知道浏览器关闭了连接,它最终会填充允许的 fpm 进程数,服务器将不再响应。

此外,由于某种原因,当max_execution_time到来时,该过程不会退出。它永远持续下去。这可能是由于我如何使用套接字来接收日志:

<?php

$host = "0.0.0.0";
$port = 1234;

header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: close');


echo "data: creating socket. pid ".getmypid().", time limit: ".ini_get('max_execution_time')."\n\n";
flush();

$socket = socket_create(AF_INET, SOCK_STREAM, 0);
if(empty($socket)) {
  $error = socket_strerror(socket_last_error());
  die("data: could not create socket: $error\n\n");
}
// Set the SO_REUSEADDR option
if (!socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1)) {
  die('data: socket_set_option() failed: ' . socket_strerror(socket_last_error($this->socket))."\n\n");
}

$success = @socket_bind($socket, $host, $port);

if(!$success) {
  $error = socket_strerror(socket_last_error());
  die("data: could not bind: $error\n\n");
}
echo "data: listening for connections\n\n";
flush();
socket_listen($socket);
$clientSocket = socket_accept($socket);

echo "data: got a connection\n\n";
flush();
socket_set_nonblock($clientSocket);

$read = [$clientSocket];
$write = null;
$except = null;
$buffer = "";

ignore_user_abort(false); // doesn't help
while(true) {
  if (connection_aborted()) { // doesn't help either
    break;
  }

  $changedSockets = $read;
  $numChanged = socket_select($changedSockets, $write, $except, 0, 800*1000); // 5000 microseconds timeout

  if($numChanged === false)
    break;
  if ($numChanged <= 0)
    continue;


  $input = socket_read($clientSocket, 16484);
  if($input === false) { // script only quts when the connection with log client is broken
    break;
  }
  $buffer .= $input;

  $lines = explode("\n", $buffer);

  if(count($lines) > 1) {
    $buffer = array_pop($lines); //last line may be incomplete
    foreach($lines as $line) {
      echo "data: $line\n\n";
    }
    flush();
  }

  if (strpos($buffer, "\n") !== false) {
    list($line, $buffer) = explode("\n", $buffer, 2);
    if(empty($line))
      echo "data: empty line\n\n";
    echo "data: $line\n\n";
    flush();
//    fastcgi_finish_request();
  }
}

socket_close($clientSocket);
socket_close($socket);

任何提示都值得赞赏!

php apache server-sent-events 长轮询 fpm

评论

0赞 Barmar 10/31/2023
把里面的循环。flush()for
0赞 NickSoft 10/31/2023
这不会改变任何事情。我不希望它刷新每一行,但要经常刷新。如果一次出现几行,最好在性能方面将它们冲洗在一起。
0赞 Barmar 10/31/2023
您需要经常冲洗以检测闭合情况。PHP 不会注意到任何事情,直到它尝试发送并收到错误。关闭连接不会通知 PHP,因此它可以立即终止脚本。
0赞 NickSoft 11/2/2023
我试过了。当然没有效果。for 循环需要 1 毫秒左右。我是在每行末尾还是每行之后刷新一次都没有关系。此外,在大多数情况下,它是一行。
0赞 Barmar 11/2/2023
如果不起作用,那么 PHP 根本无法检测到客户端何时断开连接。ignore_user_abort()

答:

0赞 NickSoft 11/2/2023 #1

它接缝代理或 fcgi 做一些缓冲,防止 php 知道浏览器已关闭连接。 我找不到有关如何更频繁地刷新代理/mod_fcgi的任何信息。

当直接将 php 作为 CGI 运行时,该问题不存在,但 cgi 效率低下。但是,服务器发送事件通常启动一次,然后运行很长时间,这减少了每次脚本启动时启动新进程的惩罚。

因此,作为一种解决方法,我在我的虚拟主机配置中执行此操作:

<FilesMatch "\.php$">
 SetHandler "proxy:unix:/usr/local/php81/var/run/xmlgen-php-fpm.sock|fcgi://localhost/"
</FilesMatch>
<Proxy "fcgi://localhost/">
ProxySet timeout=600 flushpackets=on
</Proxy>

# CGI Configuration for scripts named sse-*.php
<FilesMatch "^sse-.*\.php$">
  Action php81-cgi /cgi-bin/xmlgen/php81
  SetHandler php81-cgi
</FilesMatch>

这样,所有以“sse-”开头的 php 脚本都将通过 cgi 运行。

而且因为我浪费了很多时间来解释为什么 CGI 不起作用,所以如果您使用 suexec(apache 2.4 中的 SuexecUserGroup),那么这里是一个额外的提示,php-cgi 必须在 doc root 中(将显示它),并且它必须与 SuexecUserGroup 中的用户/组相同。实现此目的的一种方法是放置一个包装脚本:suexec -V

cat /var/www/cgi-bin/<vhostname>/php81
#!/bin/bash
PHP_CGI=/usr/local/php81/bin/php-cgi
export PHPRC="/webroot/picasse/xmlgen/etc/php.ini"
# this line irrelevant for CGI - you can skip it:
export PHP_FCGI_MAX_REQUESTS=10000
exec $PHP_CGI -c $PHPRC

并确保文件具有正确的用户/组和chownchgrpchmod +x

我很想跳过这种配置复杂性,并摆脱任何地方的缓冲。所以我仍在等待如何配置 apache 的答案,以便作为 fcgi 服务运行的 php 知道浏览器何时关闭连接。