# 0 前言

HTTP 是无状态协议，无状态协议的意思是服务端与客户端不会记录任何一次通信的信息。由于 HTTP 协议是无状态的协议，所以服务端需要记录用户的状态时，就需要用某种机制来识具体的用户，这个机制就是 Cookie 与 Session 。

# 1 什么是Session

Session 代表着服务器和客户端一次会话的过程。Session 对象存储特定用户会话所需的属性及配置信息。这样，当用户在应用程序的 Web 页之间跳转时，存储在 Session 对象中的变量将不会丢失，而是在整个用户会话中一直存在下去。当客户端关闭会话，或者 Session 超时失效时会话结束。

# 2 Session机制原理

当客户端请求创建一个 Session 时，服务端会先检查客户端的请求里面有没有带着 Session  标识（Session Id）。如果有，则说明服务器以前已为此客户端创建过 Session ，于是就根据 Session Id 把 Session 检索出来。如果没有，则为客户端创建一个 Session  并且生成一个与这个 Session 相关联的 Session Id。Session Id 将被在本次响应中返回给客户端保存。保存 Session Id 的方式大多情况下用的是Cookie。

# 3 PHP操作Session

>  参考：
>
>  [Session 官方文档](https://www.php.net/manual/en/book.session.php)
>
>  [php.ini 的 Session 配置参数文档](https://www.php.net/manual/zh/session.configuration.php)

设置 Session 时，必须先调用`session_start()`方法进行初始化，并且在这之前不能有任何输出。

## 3.1 SessionHandlerInterface

SessionHandlerInterface 定义了创建自定义会话存储需要实现的接口，即回调函数 gc()、open()、read()、write()、close()、destroy()。

![2 Session](https://cdn.learnku.com/uploads/images/202203/04/50944/v9Vk9RQbCC.jpg!large)


## 3.2 session_set_save_handler

[session_set_save_handler()](https://www.php.net/manual/en/function.session-set-save-handler.php) —— 设置用户自定义会话存储函数。

将用户自定义的会话存储类（实现了 SessionHandlerInterface 的类）的回调函数传递给 session_set_save_handler() 调用（由 PHP 内部调用）。

使用方法：用户自定义的会话存储类实例化后，将对象作为参数传给 session_set_save_handler($sessionHandlerObject) 。

例如，tp5.1 中的 Session 使用 Redis 驱动来存储会话，Redis 实现了用户自定义会话存储回调函数 。

```php
<?php

namespace think\session\driver;

use SessionHandlerInterface;
use think\Exception;

class Redis implements SessionHandlerInterface
{
    /**
     * 打开Session
     * @access public
     * @param  string $savePath
     * @param  mixed  $sessName
     * @return bool
     * @throws Exception
     */
    public function open($savePath, $sessName)
    {
        // 与 Redis 建立连接，省略代码  
    }

    /**
     * 关闭Session
     * @access public
     */
    public function close()
    {
        // 省略代码  
    }

    /**
     * 读取Session
     * @access public
     * @param  string $sessID
     * @return string
     */
    public function read($sessID)
    {
        // 省略代码  
    }

    /**
     * 写入Session
     * @access public
     * @param  string $sessID
     * @param  string $sessData
     * @return bool
     */
    public function write($sessID, $sessData)
    {
        // 省略代码  
    }

    /**
     * 删除Session
     * @access public
     * @param  string $sessID
     * @return bool
     */
    public function destroy($sessID)
    {
        return $this->handler->del($this->config['session_name'] . $sessID) > 0;
    }

    /**
     * Session 垃圾回收
     * @access public
     * @param  string $sessMaxLifeTime
     * @return bool
     */
    public function gc($sessMaxLifeTime)
    {
        return true;
    }

    /**
     * Redis Session 驱动的加锁机制
     * @access public
     * @param  string  $sessID  用于加锁的sessID
     * @param  integer $timeout 默认过期时间
     * @return bool
     */
    public function lock($sessID, $timeout = 10)
    {
    	// 省略代码  
    }

    /**
     * Redis Session 驱动的解锁机制
     * @access public
     * @param  string  $sessID   用于解锁的sessID
     */
    public function unlock($sessID)
    {
        // 省略代码  
    }
}
```



## 3.3 Session锁

基于文件的会话数据存储，在会话开始的时候都会给会话数据文件加锁， 直到 PHP 脚本执行完毕或者显式调用 session_write_close() 来保存会话数据。 在此期间，其他脚本不可以访问同一个会话数据文件。

```php
<?php
# This works in PHP 5.x and PHP 7
    
// 将 session 数据读到 $_SESSION中
session_start();

$_SESSION['something'] = 'foo';

// 关闭会话锁：写入 session 数据，关闭 session 文件，解除 session 锁
session_write_close();
```

PHP7 开始，可以设置额外的选项。这两个例子效果一样。

```php
<?php
# This only works in PHP 7

session_start(['read_and_close' => true]);

$_SESSION['something'] = 'foo';
```

# 4 Tp操作Session

[Tp5.1 看云文档](https://www.kancloud.cn/manual/thinkphp5_1/354117)

session 配置文件中的参数在代码中的使用：
```php
// use_trans_sid
ini_set('session.use_trans_sid', $config['use_trans_sid'] ? 1 : 0);

// auto_start
ini_set('session.auto_start', 0);

// use_lock：是否启用锁机制

// var_session_id：请求 Session Id 变量名，默认为 PHPSESSID。
if (isset($config['var_session_id']) && isset($_REQUEST[$config['var_session_id']])) {
    session_id($_REQUEST[$config['var_session_id']]);
} elseif (isset($config['id']) && !empty($config['id'])) {
    session_id($config['id']);
}

// name：设置 session_name
session_name($config['name']);

// path
session_save_path($config['path']);

// domain
ini_set('session.cookie_domain', $config['domain']);

// expire
ini_set('session.gc_maxlifetime', $config['expire']);
ini_set('session.cookie_lifetime', $config['expire']);

// secure
ini_set('session.cookie_secure', $config['secure']);

// httponly
ini_set('session.cookie_httponly', $config['httponly']);

// use_cookies
ini_set('session.use_cookies', $config['use_cookies'] ? 1 : 0);

// cache_limiter
session_cache_limiter($config['cache_limiter']);

// cache_expire
session_cache_expire($config['cache_expire']);

// type：设置驱动
\think\session\driver\
```



# 5 Session的垃圾回收机制

超过过期时间的 Session 数据会被视为“垃圾”，过期时间默认配置为 1400s，其配置项为：

```ini
;php.ini
session.gc_maxlifetime = 1400
```

这些过期文件需要启动 GC 机制来清除，当调用 session_start() 时触发 GC 机制。

触发概率由 gc_probability/gc_divisor 计算得出，配置项为：

```ini
;php.ini
session.gc_probability = 1
session.gc_divisor = 100
```

# 6 分布式Session

客户端发送一个请求，经过负载均衡后该请求会被分配到服务器中的其中一个，由于不同服务器含有不同的 WEB 服务器，不同的 WEB 服务器中并不能发现之前 WEB 服务器保存的 Session 信息，就会再次生成一个 Session Id，之前的状态就会丢失。

那分布式 Session 一致性问题如何解决？有以下方案：

1. Session 复制
2. Session 绑定
3. Session 共享
4. 客户端存储

## 6.1 Session复制

Session 复制是小型企业应用使用较多的一种服务器集群 Session 管理机制，在真正的开发使用的并不是很多，通过对 WEB 服务器（例如 Tomcat）进行搭建集群。

- 缺点
  1. Session 同步的原理是在同一个局域网里面通过发送广播来异步同步 Session 的，一旦服务器多了，并发上来了，Session 需要同步的数据量就大了，需要将其他服务器上的 Session 全部同步到本服务器上，会带来一定的网路开销，在用户量特别大的时候，会出现内存不足的情况。
- 优点
  1. 服务器之间的 Session 信息都是同步的，任何一台服务器宕机的时候不会影响另外服务器中 Session 的状态，配置相对简单。
  2. Tomcat 内部已经支持分布式架构开发管理机制，可以对 Tomcat 修改配置来支持 Session 复制。

## 6.2 Session绑定

利用 Nginx 的反向代理和负载均衡对客户端和服务器进行绑定，同一个客户端就只能访问该服务器，无论客户端发送多少次请求都被同一个服务器处理。

- 缺点
  1. 容易造成单点故障，如果有一台服务器宕机，那么该台服务器上的 Session 信息将会丢失。
  2. 前端不能有负载均衡，如果有，Session 绑定将会出问题。
- 优点
  1. 配置简单。

[实践：在 Docker 下，通过 Nginx 负载均衡实现 Session 绑定]()

## 6.3 Session共享

### 6.3.1 基于Redis存储

修改 php.ini 配置：

```ini
session.save_handler = redis
session.save_path = "tcp://172.16.60.2:6937?auth=这里是密码"
```

测试：

```php
<?php
    session_start();

	$_SESSION['user_info'] = ['id'=>1,'name'=>'jml'];
    print_r(session_id());  
    echo "<br>";

    print_r($_SESSION['user_info']);
```

在 Redis 可以看到结果：

```json
// 键: 
PHPREDIS_SESSION:2q9h5fjijjkh98o3k1lp5i4edj

// 值：
{
    "user_info": {
        "id": 1,
        "name": "jml"
    }
}
```

### 6.3.2 基于Memcache存储

### 6.3.3 基于数据库存储

## 6.4 客户端存储

直接将信息存储在 Cookie 中。

- 缺点
  1. 数据存储在客户端，存在安全隐患。
  2. Cookie  存储大小、类型存在限制。
  3. 数据存储在 Cookie  中，如果一次请求 Cookie  过大，会给网络增加更大的开销。