深入剖析浏览器退出之后php还会继续执行么


Posted in PHP onMay 17, 2016

前提:这里说的是典型的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是什么?

深入剖析浏览器退出之后php还会继续执行么

文档对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抓包看整个客户端和服务端交互的过程

深入剖析浏览器退出之后php还会继续执行么

这整个过程只有发送14个包,我们看下服务端第一次发送22222的时候,客户端返回的是RST。后面就没有进行后续的包请求了。

于是理解了,客户端和服务端大概的交互流程是:

当服务端在循环中第一次发送2222的时候,客户端由于已经断开连接了,返回的是一个RST,但是这个发送过程算是请求成功了。直到第二次服务端再 次想往这个socket中进行write操作的时候,这个socket就不进行网络传输了,直接返回说connection的状态已经为abort。所以 就出现了上面的情况,第一次222是status为0,第二次的时候才出现abort。

strace进行验证

我们也可以使用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 相关文章推荐
使用网络地址转换实现多服务器负载均衡
Oct 09 PHP
一个简洁的多级别论坛
Oct 09 PHP
关于PHP结束标签的使用细节探讨及联想
Mar 04 PHP
thinkphp实现like模糊查询实例
Oct 29 PHP
php判断对象是派生自哪个类的方法
Jun 20 PHP
Smarty foreach控制循环次数的一些方法
Jul 01 PHP
在Debian系统下配置LNMP的教程
Jul 09 PHP
Linux下从零开始安装配置Nginx服务器+PHP开发环境
Dec 21 PHP
Yii2创建控制器(createController)方法详解
Jul 23 PHP
CodeIgniter开发实现支付宝接口调用的方法示例
Nov 14 PHP
PHP静态方法和静态属性及常量属性的区别与介绍
Mar 22 PHP
Laravel 对某一列进行筛选然后求和sum()的例子
Oct 10 PHP
CI框架出现mysql数据库连接资源无法释放的解决方法
May 17 #PHP
CI框架集成Smarty的方法分析
May 17 #PHP
CI框架中数据库操作函数$this-&gt;db-&gt;where()相关用法总结
May 17 #PHP
CI框架中redis缓存相关操作文件示例代码
May 17 #PHP
Yii2如何批量添加数据
May 17 #PHP
PHP并发多进程处理利器Gearman使用介绍
May 16 #PHP
php截取视频指定帧为图片
May 16 #PHP
You might like
图书管理程序(一)
2006/10/09 PHP
解析在zend Farmework下如何创立一个FORM表单
2013/06/28 PHP
php递归创建目录的方法
2015/02/02 PHP
Yii2框架使用计划任务的方法
2016/05/25 PHP
yii2-GridView在开发中常用的功能及技巧总结
2017/01/07 PHP
smarty模板的使用方法实例分析
2019/09/18 PHP
yii框架结合charjs实现统计30天数据的方法
2020/04/04 PHP
js 深拷贝函数
2008/12/04 Javascript
js实现鼠标拖动图片并兼容IE/FF火狐/谷歌等主流浏览器
2013/06/06 Javascript
JS/jQ实现免费获取手机验证码倒计时效果
2016/06/13 Javascript
第七篇Bootstrap表单布局实例代码详解(三种表单布局)
2016/06/21 Javascript
Node.js+jade+mongodb+mongoose实现爬虫分离入库与生成静态文件的方法
2017/09/20 Javascript
详解各版本React路由的跳转的方法
2018/05/10 Javascript
React 组件渲染和更新的实现代码示例
2019/02/21 Javascript
javascript实现鼠标点击生成文字特效
2019/12/24 Javascript
[07:08]2014DOTA2西雅图国际邀请赛 小组赛7月11日TOPPLAY
2014/07/11 DOTA
Python标准库os.path包、glob包使用实例
2014/11/25 Python
简单介绍Python的Tornado框架中的协程异步实现原理
2015/04/23 Python
Python 模拟购物车的实例讲解
2017/09/11 Python
Numpy对数组的操作:创建、变形(升降维等)、计算、取值、复制、分割、合并
2019/08/28 Python
AVON雅芳官网:世界上最大的美容化妆品公司之一
2016/11/02 全球购物
玩具反斗城西班牙网上商城:ToysRUs西班牙
2017/01/19 全球购物
奥地利网上书店:Weltbild
2017/07/14 全球购物
三维科技面试题
2013/07/27 面试题
幼儿园儿童节活动主持词+串词大全
2014/03/21 职场文书
中班上学期幼儿评语
2014/04/30 职场文书
优秀安全员事迹材料
2014/05/11 职场文书
医院安全生产月活动总结
2014/07/05 职场文书
文明社区申报材料
2014/08/21 职场文书
4s店活动策划方案
2014/08/25 职场文书
综合素质自我评价怎么写
2014/09/14 职场文书
教师党的群众路线教育实践活动个人对照检查材料
2014/09/23 职场文书
2015年机关党委工作总结
2015/05/23 职场文书
暗恋桃花源观后感
2015/06/12 职场文书
企业培训简报范文
2015/07/20 职场文书
使用php的mail()函数实现发送邮件功能
2021/06/03 PHP