前提:这里说的是典型的lnmp结构,nginx+php-fpm的模式
如果我有个php程序执行地非常慢,甚至于在代码中sleep(),然后浏览器连接上服务的时候,会启动一个php-fpm进程,但是这个时候,如果浏览器关闭了,那么请问,这个时候服务端的这个php-fpm进程是否还会继续运行呢?
今天就是要解决这个问题。
最简单的实验
最简单的方法就是做实验,我们写一个程序:在sleep之前和之后都用file_put_contents来写入日志:
<?php file_put_contents('/tmp/test.log', '11111' . PHP_EOL, FILE_APPEND | LOCK_EX); sleep(3); file_put_contents('/tmp/test.log', '2222' . PHP_EOL, FILE_APPEND | LOCK_EX);
实际操作的结果是,我们在服务器sleep的过程中,关闭客户端浏览器,2222是会被写入日志中。
那么就意味着浏览器关闭以后,服务端的php还是会继续运行的?
ignore_user_abort
老王和diogin提醒,这个可能是和php的ignore_user_abort函数相关。
于是我就把代码稍微改成这样的:
<?php ignore_user_abort(false); file_put_contents('/tmp/test.log', '11111' . PHP_EOL, FILE_APPEND | LOCK_EX); sleep(3); file_put_contents('/tmp/test.log', '2222' . PHP_EOL, FILE_APPEND | LOCK_EX);
发现并没有任何软用,不管设置ignore_user_abort为何值,都是会继续执行的。
但是这里有一个疑问: user_abort是什么?
文档对cli模式的abort说的很清楚,当php脚本执行的时候,用户终止了这个脚本的时候,就会触发abort了。然后脚本根据ignore_user_abort来判断是否要继续执行。
但是官方文档对cgi模式的abort并没有描述清楚。感觉即使客户端断开连接了,在cgi模式的php是不会收到abort的。
难道ignore_user_abort在cgi模式下是没有任何作用的?
是不是心跳问题呢?
首先想到的是不是心跳问题呢?我们断开浏览器客户端,等于在客户端没有close而断开了连接,服务端是需要等待tcp的keepalive到达时长之后才会检测出来的。
好,需要先排除浏览器设置的keepalive问题。
抛弃浏览器,简单写一个client程序:程序连接上http服务之后,发送一个header头,sleep1秒就主动close连接,而这个程序并没有带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('/tmp/test.log', '11111' . PHP_EOL, FILE_APPEND | LOCK_EX); sleep(3); file_put_contents('/tmp/test.log', '2222' . PHP_EOL, FILE_APPEND | LOCK_EX);
发现仍然还是一样,php还是不管是否设置ignore_user_abort,会继续执行完成整个脚本。看来ignore_user_abort还是没有生效。
如何触发ignore_user_abort
那该怎么触发ignore_user_abort呢?服务端这边怎么知晓这个socket不能使用了呢?老王和diogin说是不是需要服务端主动和socket进行交互,才会判断出这个socket是否可以使用?
另外,我们还发现,php提供了connection_status和connection_aborted两个方法,这两个方法都能检测出当前的连接状态。于是我们的打日志的那行代码就可以改成:
file_put_contents('/tmp/test.log', '1 connection status: ' . connection_status() . "abort:" . connection_aborted() . PHP_EOL, FILE_APPEND | LOCK_EX);
根据手册连接处理显示我们可以打印出当前连接的状态了。
下面还缺少一个和socket交互的程序,我们使用echo,后面也顺带记得带上flush,排除了flush的影响。
程序就改成
<?php ignore_user_abort(true); file_put_contents('/tmp/test.log', '1 connection status: ' . 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('/tmp/test.log', '2 connection status: ' . connection_status() . "abort:" . connection_aborted(). PHP_EOL, FILE_APPEND | LOCK_EX); }
很好,执行我们前面写的client。观察日志:
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
终于制造出了abort。日志也显示后面几次的abort状态都是1。
但是这里有个奇怪的地方,为什么第一个2 connection status的状态还是0呢(NORMAL)。
RST
我们使用wireshark抓包看整个客户端和服务端交互的过程
这整个过程只有发送14个包,我们看下服务端第一次发送22222的时候,客户端返回的是RST。后面就没有进行后续的包请求了。
于是理解了,客户端和服务端大概的交互流程是: