브라우저가 종료된 후에도 PHP가 계속 실행되는지에 대한 심층 연구

*文
풀어 주다: 2023-03-18 19:42:01
원래의
1801명이 탐색했습니다.

브라우저를 종료한 후에도 PHP가 계속 실행되나요? 아래 편집기는 브라우저가 종료된 후에도 PHP가 계속 실행되는지 여부를 소개합니다. 그것이 모두에게 도움이 되기를 바랍니다.

전제: 여기서 말하는 것은 일반적인 lnmp 구조인 nginx+php-fpm 모드입니다.

매우 느리게 실행되는 PHP 프로그램이 있는 경우 코드에 sleep()이 포함되어 있어도 브라우저는 service 를 실행하면 php-fpm 프로세스가 시작되는데 이때 브라우저가 닫히면 서버의 php-fpm 프로세스가 계속 실행되나요?

오늘 우리는 이 문제를 해결해보겠습니다.

가장 간단한 실험

가장 간단한 방법은 실험을 하는 것입니다. 우리는 프로그램을 작성합니다: file_put_contents를 사용하여 절전 전후에 로그를 작성합니다:

<?php
file_put_contents(&#39;/tmp/test.log&#39;, &#39;11111&#39; . PHP_EOL, FILE_APPEND | LOCK_EX);
sleep(3);
file_put_contents(&#39;/tmp/test.log&#39;, &#39;2222&#39; . PHP_EOL, FILE_APPEND | LOCK_EX);
로그인 후 복사

실제 작업의 결과는 다음과 같습니다. 서버 절전 프로세스 중에 클라이언트 브라우저를 닫으면 2222가 로그에 기록됩니다.

브라우저를 닫은 후에도 서버 측 PHP가 계속 실행된다는 뜻인가요?

ignore_user_abort

Lao Wang과 diogin은 이것이 PHP의ignore_user_abort 기능과 관련이 있을 수 있음을 상기시켰습니다.

그래서 다음과 같이 코드를 약간 변경했습니다.

<?php
ignore_user_abort(false);
file_put_contents(&#39;/tmp/test.log&#39;, &#39;11111&#39; . PHP_EOL, FILE_APPEND | LOCK_EX);
sleep(3);
file_put_contents(&#39;/tmp/test.log&#39;, &#39;2222&#39; . PHP_EOL, FILE_APPEND | LOCK_EX);
로그인 후 복사

소프트웨어 사용이 없는 것으로 나타났습니다.ignore_user_abort가 어떤 값으로 설정되어 있어도 계속 실행됩니다.

하지만 질문이 있습니다. user_abort가 무엇인가요?

문서에서는 cli 모드에서의 중단에 대해 매우 명확하게 설명합니다. PHP 스크립트가 실행되고 사용자가 스크립트를 종료하면 중단이 트리거됩니다. 그런 다음 스크립트는ignore_user_abort를 기반으로 실행을 계속할지 여부를 결정합니다.

그러나 공식 문서에는 CGI 모드에서의 중단에 대해 명확하게 설명되어 있지 않습니다. 클라이언트 연결이 끊어지더라도 cgi 모드의 PHP는 중단을 수신하지 않는 것 같습니다.

ignore_user_abort는 CGI 모드에서 효과가 없나요?

심장박동 문제인가요?

가장 먼저 떠오르는 것은 심장 박동 문제인가요? 브라우저 클라이언트의 연결을 끊는 것은 클라이언트를 닫지 않고 연결을 끊는 것과 같습니다. 서버는 이를 감지하기 전에 TCP keepalive가 도착할 때까지 기다려야 합니다.

알겠습니다. 먼저 브라우저 설정에서 연결 유지 문제를 해결해야 합니다.

브라우저를 버리고 간단히 클라이언트 프로그램을 작성하세요. 프로그램이 http 서비스에 연결된 후 헤더를 보내고 1초 동안 대기한 후 연결을 적극적으로 종료합니다. 하지만 이 프로그램에는 http의 keepalive 헤더가 없습니다.

프로그램은 다음과 같습니다.

package main

import "net"
import "fmt"
import "time"

func main() {
  conn, _ := net.Dial("tcp", "192.168.33.10:10011")
  fmt.Fprintf(conn, "GET /index.php HTTP/1.0\r\n\r\n")
  time.Sleep(1 * time.Second)
  conn.Close()
  return
}
로그인 후 복사

서버 프로그램:

<?php
ignore_user_abort(false);
file_put_contents(&#39;/tmp/test.log&#39;, &#39;11111&#39; . PHP_EOL, FILE_APPEND | LOCK_EX);
sleep(3);
file_put_contents(&#39;/tmp/test.log&#39;, &#39;2222&#39; . PHP_EOL, FILE_APPEND | LOCK_EX);
로그인 후 복사

여전히 동일하다는 것을 확인했습니다. PHP는ignore_user_abort 설정 여부에 관계없이 전체 스크립트를 계속 실행합니다. ignore_user_abort가 아직 적용되지 않는 것 같습니다.

ignore_user_abort를 트리거하는 방법

그럼ignore_user_abort를 트리거하는 방법은 무엇입니까? 서버는 이 소켓을 사용할 수 없다는 것을 어떻게 알 수 있나요? Lao Wang과 Diogin은 소켓을 사용할 수 있는지 여부를 결정하기 위해 서버가 소켓과 적극적으로 상호 작용해야 하는지 물었습니다.

또한, 우리는 PHP가 현재 연결 상태를 감지할 수 있는 두 가지 메서드(connection_status 및 Connection_aborted)를 제공한다는 사실도 발견했습니다. 따라서 로깅 코드 줄을 다음과 같이 변경할 수 있습니다.

file_put_contents(&#39;/tmp/test.log&#39;, &#39;1 connection status: &#39; 
. connection_status() 
. "abort:" 
. connection_aborted() 
. PHP_EOL, FILE_APPEND | LOCK_EX);
로그인 후 복사

수동 연결 처리 화면에 따라 현재 연결 상태를 인쇄할 수 있습니다.

다음에는 여전히 소켓과 상호 작용하는 프로그램이 부족합니다. 우리는 echo를 사용하고 플러시의 영향을 제거하기 위해 나중에 플러시를 가져와야 한다는 것을 기억합니다.

프로그램이

<?php
ignore_user_abort(true);
file_put_contents(&#39;/tmp/test.log&#39;, &#39;1 connection status: &#39; . connection_status() . "abort:" . connection_aborted() . PHP_EOL, FILE_APPEND | LOCK_EX);
sleep(3);
for($i = 0; $i < 10; $i++) {
    echo "22222";
    flush();
    sleep(1);
    file_put_contents(&#39;/tmp/test.log&#39;, &#39;2 connection status: &#39; . connection_status() . "abort:" . connection_aborted(). PHP_EOL, FILE_APPEND | LOCK_EX);
}
로그인 후 복사

로 변경되었습니다. 아주 좋습니다. 앞서 작성한 클라이언트를 실행해 보세요. 관찰 기록:


1 connection status: 0abort:0
2 connection status: 0abort:0
2 connection status: 1abort:1
2 connection status: 1abort:1
2 connection status: 1abort:1
2 connection status: 1abort:1
2 connection status: 1abort:1
2 connection status: 1abort:1
2 connection status: 1abort:1
2 connection status: 1abort:1
2 connection status: 1abort:1
로그인 후 복사

드디어 중단되었습니다. 또한 로그에는 다음 몇 번의 중단 상태가 1로 표시됩니다.

근데 뭔가 이상한데 왜 처음 2번 연결 상태가 여전히 0(NORMAL)인가요?

RST

wireshark를 사용하여 패킷을 캡처하여 클라이언트와 서버 간의 전체 상호 작용 프로세스를 확인합니다.

이 전체 프로세스는 서버가 처음으로 22222를 보낼 때를 살펴보겠습니다. , 클라이언트 끝은 RST를 반환합니다. 이후 패키지 요청은 없습니다.

그러므로 클라이언트와 서버 간의 대략적인 상호 작용 프로세스는 다음과 같습니다.

서버가 루프에서 처음으로 2222를 보낼 때 클라이언트는 연결이 끊겼기 때문에 RST를 반환하지만 이 전송 프로세스는 고려됩니다. 성공. 서버가 두 번째로 이 소켓에 쓰기 작업을 수행하려고 할 때까지 이 소켓은 네트워크 전송을 수행하지 않고 연결 상태가 중단되었음을 직접 반환합니다. 그래서 위의 상황이 발생했습니다. 처음에는 222가 상태가 0이었고 중단은 두 번째에만 나타났습니다.

strace for verify

strace php -S XXX를 사용해 검증할 수도 있습니다

전체 프로세스의 strace 로그는 다음과 같습니다.

close(5)                = 0
lstat("/tmp/test.log", {st_mode=S_IFREG|0644, st_size=49873651, ...}) = 0
open("/tmp/test.log", O_WRONLY|O_CREAT|O_APPEND, 0666) = 5
fstat(5, {st_mode=S_IFREG|0644, st_size=49873651, ...}) = 0
lseek(5, 0, SEEK_CUR)          = 0
lseek(5, 0, SEEK_CUR)          = 0
flock(5, LOCK_EX)            = 0
write(5, "1 connection status: 0abort:0\n", 30) = 30
close(5)                = 0
sendto(4, "HTTP/1.0 200 OK\r\nConnection: clo"..., 89, 0, NULL, 0) = 89
sendto(4, "111111111", 9, 0, NULL, 0)  = 9
rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
nanosleep({3, 0}, 0x7fff60a40290)    = 0
sendto(4, "22222", 5, 0, NULL, 0)    = 5
open("/tmp/test.log", O_WRONLY|O_CREAT|O_APPEND, 0666) = 5
fstat(5, {st_mode=S_IFREG|0644, st_size=49873681, ...}) = 0
lseek(5, 0, SEEK_CUR)          = 0
lseek(5, 0, SEEK_CUR)          = 0
flock(5, LOCK_EX)            = 0
write(5, "2 connection status: 0abort:0\n", 30) = 30
close(5)                = 0
rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
nanosleep({1, 0}, 0x7fff60a40290)    = 0
sendto(4, "22222", 5, 0, NULL, 0)    = -1 EPIPE (Broken pipe)
--- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, si_pid=2819, si_uid=0} ---
open("/tmp/test.log", O_WRONLY|O_CREAT|O_APPEND, 0666) = 5
fstat(5, {st_mode=S_IFREG|0644, st_size=49873711, ...}) = 0
lseek(5, 0, SEEK_CUR)          = 0
lseek(5, 0, SEEK_CUR)          = 0
flock(5, LOCK_EX)            = 0
write(5, "2 connection status: 1abort:1\n", 30) = 30
close(5)                = 0
rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
nanosleep({1, 0}, 0x7fff60a40290)    = 0
open("/tmp/test.log", O_WRONLY|O_CREAT|O_APPEND, 0666) = 5
fstat(5, {st_mode=S_IFREG|0644, st_size=49873741, ...}) = 0
lseek(5, 0, SEEK_CUR)          = 0
lseek(5, 0, SEEK_CUR)          = 0
flock(5, LOCK_EX)            = 0
write(5, "2 connection status: 1abort:1\n", 30) = 30
close(5) 
。。。我们照中看status从0到1转变的地方。
...
sendto(4, "22222", 5, 0, NULL, 0)    = 5
...
write(5, "2 connection status: 0abort:0\n", 30) = 30
close(5)                = 0
rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
nanosleep({1, 0}, 0x7fff60a40290)    = 0
sendto(4, "22222", 5, 0, NULL, 0)    = -1 EPIPE (Broken pipe)
--- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, si_pid=2819, si_uid=0} ---
open("/tmp/test.log", O_WRONLY|O_CREAT|O_APPEND, 0666) = 5
fstat(5, {st_mode=S_IFREG|0644, st_size=49873711, ...}) = 0
lseek(5, 0, SEEK_CUR)          = 0
lseek(5, 0, SEEK_CUR)          = 0
flock(5, LOCK_EX)            = 0
write(5, "2 connection status: 1abort:1\n", 30) = 30
close(5)
로그인 후 복사

第二次往socket中发送2222的时候显示了Broken pipe。这就是程序告诉我们,这个socket已经不能使用了,顺便php中的connection_status就会被设置为1了。后续的写操作也都不会再执行了。

总结

正常情况下,如果客户端client异常推出了,服务端的程序还是会继续执行,直到与IO进行了两次交互操作。服务端发现客户端已经断开连接,这个 时候会触发一个user_abort,如果这个没有设置ignore_user_abort,那么这个php-fpm的程序才会被中断。

至此,问题结了。

相关推荐:

PHP 底层的运行机制与原理

php cgi与fpm关系

PHP简单实现socks5代理服务器

위 내용은 브라우저가 종료된 후에도 PHP가 계속 실행되는지에 대한 심층 연구의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:php.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿