关于 PHP 中 Session 的几个问题

什么是 Session

在 web 应用开发中,Session 被称为会话。主要被用于保存某个访问者的数据。
因为 HTTP 无状态的特色,服务端是不会记住客户端的,对服务端来讲,每个请求都是全新的。
既然如此,那么服务端怎么知道是哪一个访问者在请求它呢?又如何将不一样的数据对应上正确的访问者?答案是,给访问者一个惟一获取 Session 中数据的身份标示。php

打个比方:当咱们去超市购物时,被保安告之咱们是不能带物品进去的,必须将物品寄放在超市的储物箱中。咱们把物品交给了他,他怎么知道这些物品谁是谁的,因而他给了咱们不一样的钥匙。当咱们要取走咱们的物品时,用惟一的钥匙打开对应的箱子便可。html

就如同上面的比方同样,能够将 Session 理解为存放咱们数据的“箱子”,固然,这些“箱子”都在服务端那。服务器给访问者惟一的“钥匙”,这个“钥匙”被称做 session_id。访问者凭借本身的 session_id,就能获取到本身存在服务器端的数据。git

session_id 经过两种方式传给访问者(客户端):URL 或 cookie。详情参见:传送会话IDgithub

Session 和 Cookie 有什么关系

Cookie 也是因为 HTTP 无状态的特色而产生的技术。也被用于保存访问者的身份标示和一些数据。每次客户端发起 HTTP 请求时,会将 Cookie 数据加到 HTTP header 中,提交给服务端。这样服务端就能够根据 Cookie 的内容知道访问者的信息了。
能够说,Session 和 Cookie 作着类似的事情,只是 Session 是将数据保存在服务端,经过客户端提交来的 session_id 来获取对应的数据;而 Cookie 是将数据保存在客户端,每次发起请求时将数据提交给服务端的。web

上面提到,session_id 能够经过 URL 或 cookie 来传递,因为 URL 的方式比 cookie 的方式更加不安全且使用不方便,因此通常是采用 cookie 来传递 session_id。
服务端生成 session_id,经过 HTTP 报文发送给客户端(好比浏览器),客户端收到后按指示建立保存着 session_id 的 cookie。cookie 是以 key/value 形式保存的,看上去大概就这个样子的:PHPSESSID=e4tqo2ajfbqqia9prm8t83b1f2。在 PHP 中,保存 session_id 的 cookie 名称默认叫做 PHPSESSID,这个名称能够经过 php.ini 中 session.name 来修改,也能够经过函数 session_name() 来修改。面试

为何不推荐使用 PHP 自带的 files 型 Session 处理器

在 PHP 中,默认的 Session 处理器是 files,处理器能够用户本身实现(参见:自定义会话管理器)。我知道的成熟的 Session 处理器还有不少:Redis、Memcached、MongoDB……
为何不推荐使用 PHP 自带的 files 类型处理器,PHP 官方手册中给出过这样一段 Note:redis

不管是经过调用函数 session_start() 手动开启会话, 仍是使用配置项 session.auto_start 自动开启会话, 对于基于文件的会话数据保存(PHP 的默认行为)而言, 在会话开始的时候都会给会话数据文件加锁, 直到 PHP 脚本执行完毕或者显式调用 session_write_close() 来保存会话数据。 在此期间,其余脚本不能够访问同一个会话数据文件。浏览器

上述引用参见:Session 的基本用法安全

为了证实这段话,咱们建立一下 2 个文件:
文件:session1.php服务器

php<?php
session_start();

sleep(5);

var_dump($_SESSION);
?>

文件:session2.php

php<?php
session_start();

var_dump($_SESSION);
?>

在同一个浏览器中,先访问 http://127.0.0.1/session1.php,而后在当前浏览器新的标签页马上访问 http://127.0.0.1/session2.php。实验发现,session1.php 等了 5 秒钟才有输出,而 session2.php 也等到了将近 5 秒才有输出。而单独访问 session2.php 是秒开的。在一个浏览器中访问 session1.php,而后马上在另一个浏览器中访问 session2.php。结果是 session1.php 等待 5 秒钟有输出,而 session2.php 是秒开的。

分析一下形成这个现象的缘由:上面例子中,默认使用 Cookie 来传递 session_id,并且 Cookie 的做用域是相同。这样,在同一个浏览器中访问这 2 个地址,提交给服务器的 session_id 就是相同的(这样才能标记访问者,这是咱们指望的效果)。当访问 session1.php 时,PHP 根据提交的 session_id,在服务器保存 Session 文件的路径(默认为 /tmp,经过 php.ini 中的 session.save_path 或者函数 session_save_path() 来修改)中找到了对应的 Session 文件,并对其加锁。若是不显式调用 session_write_close(),那么直到当前 PHP 脚本执行完毕才会释放文件锁。若是在脚本中有比较耗时的操做(好比例子中的 sleep(5)),那么另外一个持有相同 session_id 的请求因为文件被锁,因此只能被迫等待,因而就发生了请求阻塞的状况。

既然如此,在使用完 Session 后,马上显示调用 session_write_close() 是否是就解决问题了哩?好比上面例子中,在 sleep(5) 前面调用 session_write_close()
确实,这样 session2.php 就不会被 session1.php 所阻塞。可是,显示调用了 session_write_close() 就意味着将数据写到文件中并结束当前会话。那么,在后面代码中要使用 Session 时,必须从新调用 session_start()

例如:

php<?php
session_start();
$_SESSION['name'] = 'Jing';
var_dump($_SESSION);
session_write_close();

sleep(5);

session_start();
$_SESSION['name'] = 'Mr.Jing';
var_dump($_SESSION);
?>

官方给出的方案:

对于大量使用 Ajax 或者并发请求的网站而言,这多是一个严重的问题。 解决这个问题最简单的作法是若是修改了会话中的变量, 那么应该尽快调用 session_write_close() 来保存会话数据并释放文件锁。 还有一种选择就是使用支持并发操做的会话保存管理器来替代文件会话保存管理器。

我推荐的方式是使用 Redis 做为 Session 的处理器。

拓展阅读:

为何不能用 memcached 存储 Session

如何使用 Redis 做为 PHP Session handler

Session 数据是何时被删除的

这是一道常常被面试官问起的问题。

先看看官方手册中的说明:

session.gc_maxlifetime 指定过了多少秒以后数据就会被视为"垃圾"并被清除。 垃圾搜集可能会在 session 启动的时候开始( 取决于 session.gc_probabilitysession.gc_divisor)。 session.gc_probabilitysession.gc_divisor 合起来用来管理 gc(garbage collection 垃圾回收)进程启动的几率。此几率用 gc_probability/gc_divisor 计算得来。例如 1/100 意味着在每一个请求中有 1% 的几率启动 gc 进程。session.gc_probability 默认为 1,session.gc_divisor 默认为 100。

继续用我上面那个不太恰当的比方吧:若是咱们把物品放在超市的储物箱中而不取走,过了好久(好比一个月),那么保安就要清理这些储物箱中的物品了。固然并非超过时限了保安就必定会来清理,也许他懒,又或者他压根就没有想起来这件事情。

再看看两段手册的引用:

若是使用默认的基于文件的会话处理器,则文件系统必须保持跟踪访问时间(atime)。Windows FAT 文件系统不行,所以若是必须使用 FAT 文件系统或者其余不能跟踪 atime 的文件系统,那就不得不想别的办法来处理会话数据的垃圾回收。自 PHP 4.2.3 起用 mtime(修改时间)来代替了 atime。所以对于不能跟踪 atime 的文件系统也没问题了。

GC 的运行时机并非精准的,带有必定的或然性,因此这个设置项并不能确保旧的会话数据被删除。某些会话存储处理模块不使用此设置项。

对于这种删除机制,我是存疑的。

好比 gc_probability/gc_divisor 设置得比较大,或者网站的请求量比较大,那么 GC 进程启动就会比较频繁。
还有,GC 进程启动后都须要遍历 Session 文件列表,对比文件的修改时间和服务端的当前时间,判断文件是否过时而决定是否删除文件。
这也是我以为不该该使用 PHP 自带的 files 型 Session 处理器的缘由。而 Redis 或 Memcached 天生就支持 key/value 过时机制的,用于做为会话处理器很合适。或者本身实现一个基于文件的处理器,当根据 session_id 获取对应的单个 Session 文件时判断文件是否过时。

为何重启浏览器后 Session 数据就取不到了

session.cookie_lifetime 以秒数指定了发送到浏览器的 cookie 的生命周期。值为 0 表示"直到关闭浏览器"。默认为 0。

其实,并非 Session 数据被删除(也有多是,几率比较小,参见上一节)。只是关闭浏览器时,保存 session_id 的 Cookie 没有了。也就是你弄丢了打开超市储物箱的钥匙(session_id)。

同理,浏览器 Cookie 被手动清除或者其余软件清除也会形成这个结果。

为何浏览器开着,我好久没有操做就被登出了

这个是称为“防呆”,为了保护用户帐户安全的。

这个小节放进来,是由于这个功能的实现可能和 Session 的删除机制有关(之因此说是可能,是由于这个功能不必定要借住 Session 实现,用 Cookie 也一样能够实现)。
说简单一点,就是长时间没有操做,服务端的 Session 文件过时被删除了。

一个有意思的事情

在我试验的过程当中,发现了小有意思的事情:我把 GC 启动的几率设置为 100%。若是只有一个访问者请求,该访问者即便过了好久(超过了过时时间)后才发起第二次请求,那么 Session 数据也仍是存在的('session.save_path' 目录下面的 Session 文件存在)。是的,明明就超过了过时时间,却没有被 GC 删除。这时,我用另一个浏览器访问时(相对于另外一个访问者),此次请求生成了新的 Session 文件,而上一个浏览器请求生成的那个 Session 文件终于没有了(以前那个 Session 文件在 'session.save_path' 目录下面的消失了)。

还有,发现 Session 文件被删除后,再次请求,仍是会生成和以前文件名相同的 Session 文件(由于浏览器并无关闭,再次请求发送的 session_id 是相同的,因此从新生成的 Session 文件的文件名仍是同样的)。可是,我不理解的是:这个从新出现的文件的建立时间居然是第一次的那个建立时间,难道它是从回收站中回来的?(确实,我作这个试验时是在 window 下进行的)

我猜想的缘由是这样:当启动会话后,PHP 根据 session_id 找到并打开了对应的 Session 文件,而后才启动 GC 进程。GC 进程就只检查除了当前这个 Session 文件外的其余文件,发现过时的就干掉。全部,即便当前这个 Session 文件已通过期了,GC 也没有删除它。

我认为这个不合理的。

因为发生这种状况影响也不大(毕竟线上请求不少,当前请求的过时文件被其余请求唤起的 GC 干掉的可能性是比较大的) + 我没有信心去看 PHP 源代码 + 我并不在线上使用 PHP 自带的 files 型 Session 处理器。因此,这个问题我就没有深刻研究了。请谅解。

php<?php
// 过时时间设置为 30 秒
ini_set('session.gc_maxlifetime', '30');

// GC 启动几率设置为 100%
ini_set('session.gc_probability', '100');
ini_set('session.gc_divisor', '100');

session_start();
$_SESSION['name'] = 'Jing';

var_dump($_SESSION);
?>

拓展阅读:

如何设置一个严格 30 分钟过时的 Session

相关文章
相关标签/搜索