另类方式实现PHP后台运行

2022-10-11 21:51:25 171 0
魁首哥

问题

开发中经常会遇到这种情况:当用户触发某个请求后,需要PHP做一些处理,但是不需要用户等待处理完成,也就是请求需要快速响应并结束,但结束后需要PHP在运行一段时间做一些收尾的处理。

比如用户做某个操作后,需要发邮件,这里假设没有消息队列,而是直接通过smtp进行发送,由于发送邮件建立tcp连接很耗时,而用户浏览器端的请求一直在等待服务端响应结束,给用户的体验是页面一直在加载中,卡在那里了,所以可以考虑后端先正常结束响应,让用户“感觉”操作已经成功结束,然后PHP再继续运行一定时间去发送邮件。

办法

这个方法使用到的是HTTP的特性,先整理一下思路:

  1. HTTP是无状态的HTTP是请求-应答模式HTTP是建立在TCP之上的浏览器在请求一个web资源时(本文指PHP文件)会等待Web服务器的响应(本文中指 Apache )直到响应结束如果在等待的过程中用户点击了浏览器上的停止按钮,浏览器会关掉TCP连接,也就是中止当前的HTTP的请求-应答过程,根据对TCP的理解,这个中止是向服务器端发送了一条TCP指令底层连接TCP断掉,当前未完成的响应输出不到浏览器上

上面几条都好理解,但第4点还有细节:

  1. web服务器响应http头中有一个头信息:Connection 会告知浏览器连接的保持情况,一般情况下都是:
  2. Connection:keep-alive
  3. 并且还有另外一个头说了要保持多久:Keep-Alive:300。那如果服务器的响应中说连接已经关闭(connection: close )了会发生什么呢?浏览器会停止等待响应,(rfc 2616)

如果在用户请求的PHP中输出HTTP头:Connection:close。并把这些头输出到浏览器,然后再继续执行后面的代码,会是什么效果呢?

反应到浏览器上就是页面请求完了,但是php并没有执行完,也就是将继续执行(执行时间受php的代码逻辑和php.ini设置的最大运行时间限制),这就实现了浏览器及所请求的php异步执行的效果

例子:

ob_end_clean();
#清除之前的缓冲内容,这是必需的,如果之前的缓存不为空的话,里面可能有http头或者其它内容,导致后面的内容不能及时的输出 

 header ("Connection: close");
#告诉浏览器,连接关闭了,这样浏览器就不用等待服务器的响应 
#可以发送200状态码,要不然可能浏览器会重试,特别是有代理的情况下 

ob_start();#开启当前代码缓冲

//{{逻辑代码
 echo  "一些处理";
//逻辑代码}}

//下面输出http的一些头信息
$size=ob_get_length(); 
header("Content-Length: $size"); 
ob_end_flush();#输出当前缓冲 
flush();#输出PHP缓冲

#休眠PHP,也就是当前PHP代码的执行停止,1秒钟后PHP被唤醒, 
#PHP唤醒后,继续执行下面的代码,但这个时候上面代码的结果已经输出浏览器了, 
#也就是浏览器从HTTP头中知道了服务端关闭了连接,浏览器将不在等待服务器的响应, 
#反应给客户的就是页面不会显示处于加载状态中,换句话说用户可以关掉当前页面,或者关掉浏览器, 
#PHP唤醒后继续执行下面的代码,这也就实现了PHP后台执行的效果, 
#休眠的作用只是演示先把前面的输出作完,不要急于马上执行下面的代码,休息一下而已,也就是说下面的代码 
#执行的时候前面的输出应该到达浏览器了
sleep(1);

echo '这里的输出用户看不到,后台运行的';

//下面代码的任何输出都不会输出给浏览器,因为http连接已经关了, 
//所以下面的代码的执行属于后台运行的

set_time_limit(0);#不受时间限制
$f = fopen('1.txt', 'a+'); 
for($i=0; $i < 1000; $i++){ 
 if (fwrite($f,$i."") === FALSE) { 
 echo "Cannot write to file ($filename)"; 
 } 
}
fclose($f);

 

其它情况

这种做法是让PHP主动告诉浏览器结束对话,这个过程应该是很快的,PHP收到请求后马上发送http给浏览器,但有时候的情况是PHP要先做一些事情,然后在把连接断掉的http响应返回给浏览器,但如果这个时候出现了网络或者其它一些意外情况导致了浏览器关掉了或者失去与服务器了连接了,PHP的响应头输出到不了浏览器上,PHP还会继续执行吗?

与浏览器作请求-应答这个过程的是Web服务器,如果请求的是PHP或者其它服务端语言,根据服务器的配置(loadmodule addtype这些)这些资源的请求会转到对应的语言处理器上,如所有的.php访问都会由PHP解析执行,并把执行的结果返回给 Apache,apache在返回给浏览器,但如果这些响应输出不到浏览器(浏览器端中止链接了),apache会通知PHP,PHP就会中止当前请求文件的执行。

也就是说如果一个请求的过程中用户关掉了浏览器,或者点停止按钮的话,所请求的php代码可能就会只执行到一半,没有执行完,如果这个时候是在做一些数据库的写操作,数据就可能没有写完全。

在这种情况下如果PHP仍然要继续执行,可以使用PHP的一些连接控制函数来忽略客户端退出连接的情况:

ignore_user_abort,该函数表示客户端断掉后是否要中止PHP的执行。默认是中止

换句话说,在请求的过程中浏览器是否非正常退出PHP是可以知道的,可能通过connection_status()来得到连接状态:

  • 0 – 正常
  • 1 – 中止
  • 2 – PHP执行超时

这三个状态可以叠加,也就是可以有 3 – 中止+PHP执行超时

问题是我们在什么时候调用connection_status()来得到连接的状态呢,在一般的代码中调用,得到的都是0,但如果浏览器中止了,php的执行也中止了,在一般的代码中这个函数不能很好的看到预期的结果,PHP提供了一个hook:register_shutdown_function该方法用于注册请求结束时的回调,不管请求是正常结束还是异常结束,只要PHP在执行这个回调是一定会调用到。可以在该方法中查看 connection_status()返回值,

function shutdown(){ 
 $f = fopen('1.txt','a+'); 
 fwrite($f,connection_status()); 
}
register_shutdown_function('shutdown');
while(1){ 
 #如果注掉,用户点了停止,PHP也会执行到超时,文件1.txt中写入的是2 PHP执行超时 
 #如果不注掉,用户点了停止,文件1.txt中写入的是1 - 用户中止 
 echo ++$i."
"; }

收藏
分享
海报
0 条评论
171
上一篇:3分钟短文 | Laravel 动态修改 env 环境变量的值 下一篇:用PHP判断日期是节假日还是工作日?

本站已关闭游客评论,请登录或者注册后再评论吧~

忘记密码?

图形验证码