通过函数session_set_save_handler()分析PHP内核对于session的处理过程

session_set_save_handler

我们知道在php.inisession.save_handler可以设置session的处理机制,默认为file,也支持redis,这只是从语言层面去支持常用的技术,但是总有人会去使用一些自己开发的闭源的工具,从程序设计上来讲应该给予一把钥匙可以使你灵活的自定义 session 处理机制,这就是 session_set_save_handler 函数。

函数 session_set_save_handler 可以设置用户自定义会话存储函数,允许开发者针对session操作的每一个步骤设置自定义的回调函数。将不会额外的创建 session 文件。

直接上代码:SessionService.php

<?php

class SessionService implements SessionHandlerInterface
{
    public $redis;
    public $sessionExpire = 30;
    public $log = "/www/logs.log"; // 使用绝对路径,否则在 write 和 close 时会写入到别的文件去了。

    public function __construct()
    {
        $this->redis = new \Redis();
        $this->redis->connect('r-m5e0hhv80jlu18d3ccpd.redis.rds.aliyuncs.com', '6379');
        $this->redis->auth("testRedis_7sqB81LoH");
        $this->redis->select(9);
    }

    /**
     * 打开会话时被调用
     * @param $savePath 配置的 session.save_path
     * @param $sessionName 配置的 session.name
     * @return bool
     */
    public function open($savePath, $sessionName)
    {
        file_put_contents($this->log, 'open' . PHP_EOL, FILE_APPEND);
        return true;
    }

    /**
     * 读取会话数据时被调用
     * @param $sessionId
     * @return bool|string
     */
    public function read($sessionId)
    {
        file_put_contents($this->log, 'read' . PHP_EOL, FILE_APPEND);
        $data = $this->redis->get($sessionId);
        if ($data) {
            return $data;
        } else {
            return '';
        }
    }

    /**
     * 写入会话数据时被调用
     * @param $sessionId
     * @param $data
     * @return bool
     */
    public function write($sessionId, $data)
    {
        file_put_contents($this->log, 'write' . PHP_EOL, FILE_APPEND);
        if ($this->redis->setex($sessionId, $this->sessionExpire, $data)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * 关闭会话时被调用
     * @return bool
     */
    public function close()
    {
        file_put_contents($this->log, 'close' . PHP_EOL, FILE_APPEND);
        return true;
    }

    /**
     * 销毁会话时被调用
     * 当调用 session_destroy() 函数, 或者调用 session_regenerate_id() 函数并且设置 destroy 参数为 true 时, 会调用此回调函数
     * @param $sessionId
     * @return bool
     */
    public function destroy($sessionId)
    {
        file_put_contents($this->log, 'destroy' . PHP_EOL, FILE_APPEND);
        if ($this->redis->delete($sessionId)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * php执行会话清理算法时会被调用,调用周期由 session.gc_probability 和 session.gc_divisor 参数控制
     * @param $lifetime 配置的 session.gc_maxlifetime 秒
     * @return bool
     */
    public function gc($lifetime)
    {
        file_put_contents($this->log, 'gc' . PHP_EOL, FILE_APPEND);

        // cleanup old sessions -- todo

        return true;
    }
}

session_set.php

<?php
$handler = new SessionService();

// PHP > 5.4
session_set_save_handler($handler);
session_start();

$_SESSION['username'] = 'raoxiao';

echo $_SESSION['username'];

起初只能打印出 open, read, write, close

设置配置:

<?php
    
$handler = new SessionService();

// PHP > 5.4
session_set_save_handler($handler);

ini_set('session.gc_maxlifetime', 10);
ini_set('session.gc_probability ', 1);
ini_set('session.gc_divisor', 5);

session_start();

$_SESSION['username'] = 'raoxiao';

echo $_SESSION['username'];

可以打印出 open, read, write, close, gc

代码中并没使用序列化反序列化操作,这是因为PHP会自动调用 session_encode() 和 session_decode() 方法处理,他们是不同与 serialize的序列化方法。

session 运行流程

实际上,session 运行流程为:

  1. open
  2. read
  3. 触发概率性gc
  4. 脚本运行完毕
  5. 收集 $_SESSION
  6. 关闭脚本,关闭输出流
  7. 内核执行write
  8. 内核执行close

所以内核将 session 信息读取到全局变量$_SESSION,然后我们来操作变量$_SESSION实现增删改查,最终在脚本执行完毕后内核又将 $_SESSION 内的数据重新序列化后写入文件,而不管 session 内容是否有变化,于是 session 文件的修改时间就发生变化,我们设置的 session 过期时间对比的是 session 文件的最后修改时间。

其实,$_SESSION['username'] = 'rao';只是往一个全局变量里面写入值,echo $_SESSION['username'];只是读取变量的值,都跟 session 操作无关。

PHP 会在脚本执行完毕或调用 session_write_close() 函数之后调用 write。 注意,在调用完此回调函数之后,PHP 内部会调用 close 回调函数。PHP 会在输出流写入完毕并且关闭之后 才调用 write 回调函数, 所以在 write 回调函数中的调试信息不会输出到浏览器中。 如果需要在 write 回调函数中使用调试输出, 建议将调试输出写入到文件。

有流程可看出,gc 是在请求到来时触发的,在 file 模式下会存在性能问题。

session 并发问题

既然 session 是基于文件的,那么如何解决文件的并发写问题呢?在函数 session_write_close 的解释中已经说明了这一点,php使用的加锁的方式来确保并发安全的。

test_session.php

<?php

session_start();
$_SESSION["name"] = "raoxiaoya";
echo $_SESSION["name"], PHP_EOL;
sleep(20);

test_session_2.php

<?php

session_start();
$_SESSION["name"] = "raoxiaoya123";
echo $_SESSION["name"], PHP_EOL;

先访问 test_session.php再访问test_session_2.php,发现两个都阻塞了,在 web 项目中会存在这种情况,因为浏览器是并行的发出请求的,当开启了 session ,且同时有多个请求时,你会发现后面的请求要等到前面的请求结束后才会返回,并不像我们知道的那样(响应时间短的先返回)。

解决这种情况的方法是使用函数session_write_close提前写入文件结束会话,当然前提是后面不再需要操作 session 否则会出现 session 信息混乱的问题。修改test_session.php

<?php

session_start();
$_SESSION["name"] = "raoxiaoya";
echo $_SESSION["name"], PHP_EOL;

session_write_close();

sleep(20);
如何精确的控制会话时间

从上面的知识点我们知道 session 本身的机制是做不到精确的,我们可以使用 redis 作为存储介质使其自动过期,或者在 session 中添加 expire 信息,每次读取的时候做个判断。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值