一、问题
浏览器和服务器之间是通过HTTP进行通信的,浏览器发送请求给服务器,服务器处理完请求后,发送响应结果给浏览器,浏览器展示给用户。如果服务器处理请求时间比较长,那么浏览器就需要等待服务器的处理结果。
但是,有时候,浏览器不需要等待服务器的处理结果,只要发送的请求已经被服务器接收到。所以,这种情况下,浏览器希望服务器接收到请求立即返回一个响应,比如字符串'success'。这样浏览器可以继续执行后续代码。
在PHP中,很容易做到。如果服务器使用的是nginx+fpm,可以使用下面的代码:
echo 'success';
fastcgi_finish_request();
// 执行耗时代码.....
如果服务器使用的是apache,可以使用下面的代码:
ob_end_flush();
ob_start();
echo 'success';
header("Content-Type: text/html;charset=utf-8");
header("Connection: close");
header('Content-Length: '. ob_get_length());
ob_flush();
flush();
// 执行耗时代码.....
二、测试
一个静态页面:index.html,代码如下:
<html>
<head><title>测试PHP提前输出响应</title></head>
<body>
<script src="http://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
<script>
function getCurTime(d) {
var time = d.getFullYear() + "-" +(d.getMonth()+1) + "-" + d.getDate() + " " + d.getHours() + ":" + d.getMinutes() + ":" + d.getSeconds();
return time;
}
function getOther(url) {
$.ajax({
url: url,
success:function(data){
console.log(data);
console.log(getCurTime(new Date()));
}
});
}
console.log(getCurTime(new Date()));
$.ajax({
url: "http://www.test.com/test/1.php",
success:function(data){
console.log(data);
console.log(getCurTime(new Date()));
getOther("http://www.test.com/test/2.php");
}
});
</script>
</body>
</html>
浏览器首先通过ajax请求a.php,该php文件执行是比较耗时的,要等到a.php的响应后继续请求b.php文件。如果a.php不提前输出响应,ajax必须等待响应后才能去请求b.php。
上面代码,会在每次请求发送开始和返回时打印出时间。
两个PHP文件:1.php 、 2.php
a.php文件内容:
<?php
echo 'success';
sleep(5);
?>
上面代码使用sleep函数模拟耗时任务,耗时5秒。
b.php文件内容:
<?php
echo 'this is b.php';
?>
浏览器执行index.html,查看console日志如下:
发现第二次请求时间比第一次请求时间迟5秒,正好是a.php文件的执行时间,说明浏览器需要等待a.php文件执行完才能去请求b.php文件。
下面修改a.php文件内容,提前输出响应,代码如下:
<?php
if(!function_exists('fastcgi_finish_request')) {
ob_end_flush();
ob_start();
}
echo 'success';
if(!function_exists('fastcgi_finish_request')) {
header("Content-Type: text/html;charset=utf-8");
header("Connection: close");
header('Content-Length: '. ob_get_length());
ob_flush();
flush();
} else {
fastcgi_finish_request();
}
sleep(5);
?>
浏览器执行index.html,查看console日志如下:
两次请求的时间都一样,表示a.php已经提前输出响应了。
三、session问题
开发web应用程序不可避免的要用到session,修改一下a.php文件内容如下:
<?php
session_start();
$_SESSION['uname'] = 'jianshu';
if(!function_exists('fastcgi_finish_request')) {
ob_end_flush();
ob_start();
}
echo 'success';
if(!function_exists('fastcgi_finish_request')) {
header("Content-Type: text/html;charset=utf-8");
header("Connection: close");
header('Content-Length: '. ob_get_length());
ob_flush();
flush();
} else {
fastcgi_finish_request();
}
sleep(5);
?>
修改b.php文件内容如下:
<?php
session_start();
echo $_SESSION['uname'];
echo 'this is b.php';
?>
浏览器执行index.html,查看console日志如下:
发现第一次和第二次请求时间一样,说明第一次请求已经提前输出响应了,但是第二次请求的响应时间却比请求发送时间迟5秒。第二次请求的是b.php文件,b.php文件内容只有几行代码,为什么会执行5秒才返回。
原因是,session是有锁的。为防止并发的写session数据,PHP自带的的文件保存session数据是加了一个互斥锁。程序执行session_start(),此时当前程序就开始持有锁。程序结束,此时程序自动释放Session的锁。
浏览器第一次请求a.php文件,对session文件进行加锁,虽然提前输出响应,但程序还在执行,session文件的锁还未释放。所以,第二次请求b.php文件时,由于b.php文件的开始就要打开session文件,但session文件的锁还未释放,需要等待a.php执行完,也即要等待5秒。
一般session文件跟SESSION_ID有关,每个SESSION_ID会创建一个session文件,下面可以看到两次请求带过去的SESSION_ID是一样的。
如何解决session文件加锁的问题,可以使用session_write_close函数,该函数的作用就是数据写入session文件并结束会话。
修改a.php文件,在使用session代码后面加上session_write_close函数。
<?php
session_start();
$_SESSION['uname'] = 'jianshu';
session_write_close();
............
浏览器执行index.html,查看console日志如下:
发现没有等待存在了。实际开发中,使用完session,可以加上session_write_close()函数,减少服务器开销。