提问人:NickSoft 提问时间:10/30/2023 最后编辑:NickSoft 更新时间:11/8/2023 访问量:40
PHP脚本在浏览器连接关闭时不会退出
PHP script doesn't quit when browser connection is closed
问:
注意:这不是我使用 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);
任何提示都值得赞赏!
答:
它接缝代理或 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
并确保文件具有正确的用户/组和chown
chgrp
chmod +x
我很想跳过这种配置复杂性,并摆脱任何地方的缓冲。所以我仍在等待如何配置 apache 的答案,以便作为 fcgi 服务运行的 php 知道浏览器何时关闭连接。
评论
flush()
for
ignore_user_abort()