共计 21123 个字符,预计需要花费 53 分钟才能阅读完成。
前言:
在不久之前,本人去参加了某公司的实习面试,其中 HR 问我关于 SESSION 实现的原理,当时我就懵逼了,因为在之前的开发中,我只知道 session 与 cookie 的区别在于:session 是保存在服务器端,cookie 保存在客户端。那 session 在服务端是怎么样保存的?session_id 又是什么?等等。我当时答不上来。回来后决定把这些搞懂。
为什么要使用 SESSION?
是因为目前网络中所使用的 http 协议造成的,http 协议是无状态协议,通俗点说就是当你发送一次请求道服务器端,然后再次发送请求到服务器端,服务器是不知道你的这一次请求和上一次请求是来源于同一个人发送的。而 session 就能很好解决这个问题。
在我们的访问期间,各个页面间共享的数据放在 session 中,就比如说我们的登陆信息,如果没有 session 的话,当你在这个页面登陆之后,在点击下一个页面的时候你需要再次登陆。
引入:
现在我们来看看平时我们是怎么使用 session 的,大家看下面的例子:
<?php
#index.php 文件
session_start(); // 启动会话
$user = isset($_GET['user'])?$_GET['user']:"default"
if(!isset($_SESSION['user'])){$_SESSION['user'] = $user; // 设置会话
}
var_dump($_SESSION);
unset($_SESSION['user']); // 清除会话
现在我们在浏览器 A 打开 http://localhost/index.php?user=lsgogroup;
返回:array(1){["user"]=>string(9)"lsgogroup"}
在浏览器 B 打开 http://localhost/index.php
返回:array(1){["user"]=>string(7)"default"}
问题:
- session_start() 的作用是什么?
- 为什么在浏览器 B 中返回的不是:array(1){[“user”]=>string(9)”lsgogroup”}?
- $_SESSION 数组是怎么保存这些数据的?
理解 PHP SESSION 机制:
session 机制是一种服务器端的机制,服务器使用一种类似于散列表的结构来保存信息。
当程序需要为某个客户端的请求创建一个 session 的时候,服务器首先检查这个客户端的请求(Http Request)里是否已包含了一个 session 标识 - 称为 sessionid,如果已包含一个 sessionid 则说明以前已经为此客户端创建过 session,服务器就按照 sessionid 把这个 session 检索出来使用,如果客户端请求不包含 sessionid,则为此客户端创建一个 session 并且生成一个与此 session 相关联的 sessionid,sessionid 的值应该是一个既不会重复,又不容易被找到规律以仿造的字符串,这个 sessionid 将被在本次响应中返回给客户端保存。而这个 sessionid 就是作为客户端的唯一标识而存在的(即使在同一台电脑上,浏览器 A 和浏览器 B 对于服务器来说都是不同的客户端)。
上面一段话你可能暂时不会理解,不过不要紧,我会在下面作出解释:
现在我们来看看浏览器 A 和 浏览器 B 的 cookie:
浏览器 A(这里对应是谷歌浏览器):
浏览器 B(这里对应是火狐浏览器):
对比可以看到,两个浏览器对于 localhost 都有一条名为 PHPSESSID 的 cookie 记录,而这个 PHPSESSID 就是上面所说的 sessionid,它告诉服务器请求是来自浏览器 A 还是浏览器 B。
现在我们可以回答上面的问题 2 了:
由于浏览器 A 的 PHPSESSID 和浏览器 B 的 PHPSESSID 是不一样的,因此服务器根据 sessionid 检索 session 的数据也是不一样的,也就是说浏览器 A 请求的 $_SESSION 数组和 浏览器 B 请求的 $_SESSION 数组也是不一样的。
(当然,PHPSESSID 这个 id 名不是固定的,我们可以在 PHP.ini 文件中的 session.name 项进行修改。)
上面的例子是使用 COOKIE 保存 PHPSESSID,但是,由于 cookie 可以被人为的禁止,必须有其他机制以便在 cookie 被禁止时仍然能够把 sessionid 传递回服务器。有两种技术可以解决这个问题:
- URL 重写,就是把 sessionid 直接附加在 URL 路径的后面:http://localhost/index.php?user=lsgogroup&PHPSESSID=ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng
- 隐藏表单传递。
由于这不是重点,这里不展开讲。
SESSION 是怎么存储数据的?
答:session 是以文件的形式保存的。
php.ini 中的配置项 session.save_handler = files;
默认为 file,定义 session 在服务端的保存方式,file 意为把 session 保存到一个临时文件里。
php.ini 中的配置项 session.save_path=“”;
这个里面填写的路径,将会使 session 文件保存在该路径下。
session 文件的命名格式是:”sess_[PHPSESSID 的值]”。每一个文件,里面保存了一个会话的数据。
我们查看服务器端 session.save_path 目录会发现很多类似 sess_vv9lpgf0nmkurgvkba1vbvj915 这样的文件,这个其实就是 sessionid(也就是 PHPSESSID)“vv9lpgf0nmkurgvkba1vbvj915″ 对应的数据。真相就在这里,客户端将 sessionid 传递到服务器,服务器根据 sessionid 找到对应的文件,读取的时候对文件内容进行反序列化就得到 session 的值($_SESSION 数组中的数据),保存的时候先序列化再写入。
由于我做实验的时候使用的是 Ubuntu 系统,因此我的 session.save_path 默认实在 /var/lib/php/sessions 下,我们来看看前面浏览器 A 生成的 session 文件是怎样的(浏览器 A 的 PHPSESSID =‘nqqleletmsb0nuf7d4ulvotk45’):
cd /var/lib/php/sessions
# 由于 session 数据是很重要的数据,因此必须只能 root 用户才能打开
sudo vim sess_nqqleletmsb0nuf7d4ulvotk45
# 看看文件格式是不是 "sess_[PHPSESSID 的值]"
文件内容:
user|s:9:"lsgogroup";
从文件内容可以看到,数据是经过序列化的,数据的读取规则是这样的:
- 每一个 session 的值是以分号”;”分开的。比如”user|s:9:”lsgogroup”;“就是一个完整的 session 值结束,如果再添加 $_SESSION[‘name’]=”LSGOZJ”,则变成这样”user|s:9:”lsgogroup”;name|s:7:”LSGOZJ”;“
- 里面的读取规则:符号“|”前面表示 session 名称。符号后面是该 session 的具体信息。包括:数据类型,字符长度,内容。比如说”user|s:9:”lsgogroup”;“,$_SESSION[‘user’] 的值是“lsgogroup”,是一个长度为 9 的字符串。
- 等等。。。
到了这里,我们就解决了上面的问题 3 了。
其实还有很多种存储 session 的方式,如果我们想自定义别的方式保存(比如用数据库),则需要把该项设置为 user;我们还可以使用 memcache、Redis 等优秀的缓存系统(前提是你的服务器安装了此类软件)。
session_start()函数的作用是什么?
了解的原理之后,所谓的 session 其实就是客户端一个 sessionid 对应服务器端一个 session file,新建 session 之前执行 session_start() 是告诉服务器要种一个 cookie 以及准备好 session 文件,要不然你的 session 内容怎么存;读取 session 之前执行 session_start() 是告诉服务器,赶紧根据 sessionid 把对应的 session 文件反序列化。
说白了,当我们使用 php 的内置函数 session_start() 的时候,就是到服务器的指定的磁盘目录把 session 数据载入,实际上就是拿类似 sess_74dd7807n2mfml49a1i12hkc45 的文件。
只有一个 session 函数可以在 session_start() 之前执行,session_name():读取或指定 session 名称(比如默认的就是”PHPSESSID”),这个当然要在 session_start 之前执行。
根据 http 的请求机制,当浏览器请求的时候,头部信息会把浏览器中的 cookie 一起发给服务器。PHPSESSID 这个 cookie 也是在其中发给了服务器,php 引擎通过读取 PHPSESSID 的值来确定要载入哪个 session 文件。
比如值为 74dd7807n2mfml49a1i12hkc45,载入的就是”sess_74dd7807n2mfml49a1i12hkc45”。
注:当你调用 php 的函数 session_start(), 才表明你需要使用 session 文件了。不然平白无故就去载入文件,浪费性能。
SESSION 的清理:
在平时我们谈论 SESSION 的机制的时候,常常听到这样一种误解“只要关闭浏览器,session 就消失了”(本人也是一度认为这样),其实可以想象一下会员卡的例子,除非顾客主动对店家提出销卡,否则店家绝对不会轻易删除顾客的资料。
对 session 来说也是一样的,除非程序通知服务器删除一个 session,否则服务器会一直保留,程序一般都是在用户做 logoff(注销操作,类似于 session_destroy()操作)的时候发个指令去删除 session。然而浏览器从来不会主动在关闭之前通知服务器它将要关闭,因此服务器根本不会有机会知道浏览器已经关闭,之所以会有这种错觉,是大部分 session 机制都使用会话 cookie 来保存 sessionid,而关闭浏览器后这个 sessionid 就消失了,再次连接服务器时也就无法找到原来的 session,但是服务器上对应的 session file 依然存在。
为什么关闭浏览器后 sessionid 就会消失呢?这跟 cookie 在客户端的存储有关,如果在设置 cookie 的时候没有指定生命周期,那么 cookie 的数据是存储在内存中的,当浏览器被关闭,内存被回收了,那么 cookie 也就没有了(这就是为什么 cookie 在没有指定生命周期的时候,其生命周期与浏览器生命周期一样)。
如果服务器设置的 cookie 被保存到硬盘上(设置了生命周期),或者使用某种手段改写浏览器发出的 HTTP 请求头,把原来的 sessionid 发送给服务器,则再次打开浏览器仍然能够找到原来的 session。
恰恰是由于关闭浏览器不会导致 session 被删除,迫使服务器为 seesion 设置了一个失效时间,当距离客户端下一次使用 session 的时间超过这个失效时间时,服务器就可以认为客户端已经停止了活动,才会把 session 删除以节省存储空间。
我们来看看服务器是怎样删除 session 数据的:
session.gc_probability = 1
session.gc_divisor = 100
session.gc_maxlifetime = 1440
这三个配置项组合构建服务端 session 的垃圾回收机制。
session.gc_probability 与 session.gc_divisor 构成执行 session 清理的概率,理论上的解释为服务端定期有一定的概率调用 gc(garbage collection 垃圾回收)进程来对 session 进行清理,清理的概率为:gc_probability/gc_divisor 比如:1/100 表示每一个新会话初始化时,有 1% 的概率会启动垃圾回收程序,清理的标准为 session.gc_maxlifetime 定义的时间(清理过期的数据)。
我所用的系统是 ubuntu,php.ini 中指定的 session.gc_probability = 0,也就是概率为零,原因是该系统是使用 cron 脚本来执行垃圾清理的。
后话:
session 还有很多需要整理和学习的地方,如:
- session 多服务器共享的问题,假如有多台 php 服务器进行负载均衡的时候,用户登录时访问的是第一台服务器,没准下一个页面访问的是第二台服务器,但是 session 数据是存储在第一台服务器上的,因此在访问下一个页面的时候由于没有 session 数据(第二台服务器上)导致用户必须重新登陆。
- 从上面的分析我们也知道,php 中 session 默认通过文件的方式实现,但是如果访问量大,可能产生的 SESSION 文件会比较多,从众多的文件中选择其中一个文件不是一件轻松的事情,而且每次都以打开文件、读取文件的方式,也会产生大量的 I/O 操作,严重影响服务器的性能。
本文参考了很多网上的资源,加上自己的理解,由于篇幅已经很长,因此上面所提到的问题我会在后续文中解决。
更多详情见请继续阅读下一页的精彩内容:http://www.linuxidc.com/Linux/2017-05/143594p2.htm
前言:
本篇文章是以我之前一页文章《对 PHP SESSION 的深入理解(一)》为基础的,如果你没有阅读该篇文章,建议你回头看看。
在《对 PHP SESSION 的深入理解(一)》的最后,我们提到在应用 session 时可能会遇到的问题:
- session 多服务器共享的问题,假如有多台 php 服务器进行负载均衡的时候,用户登录时访问的是第一台服务器,没准下一个页面访问的是第二台服务器,但是 session 数据是存储在第一台服务器上的,因此在访问下一个页面的时候由于没有 session 数据(第二台服务器上)导致用户必须重新登陆。
- 从前面的分析我们也知道,php 中 session 默认通过文件的方式实现,但是如果访问量大,可能产生的 SESSION 文件会比较多,从众多的文件中选择其中一个文件不是一件轻松的事情,而且每次都以打开文件、读取文件的方式,也会产生大量的 I/O 操作,严重影响服务器的性能。
在这篇文章中将讨论解决的方法。
问题一讨论:
针对第一个问题,我在这里引入《session 多服务器共享的方案梳理》中的例子:
假设这种情况:多台 PHP 服务器进行负载均衡的时候,比如有三台 php 服务器,为了实现负载均衡,那么三台服务器上面的 php 代码都是一样(拷贝一份)。如下图:
上面的图是 nginx+fpm 部署图。可以看到多台 php 服务器进行负载均衡。
根据之前对 session 原理的理解,session 的数据默认是保存在磁盘文件中,而且 session 是 php 引擎生成的,那么生成 session 数据文件都是存储在本地了(a,b,c 各自的服务器磁盘上)。负载均衡的目的本来就是要为了平均分配请求,所以没有固定第一次访问和第二次访问是同一台服务器,实际上无法确定的。第一秒访问可能是 a 服务器,第二秒访问的可能是 c 服务器。
所以同一个登录会员,实际上就会出现:第一秒访问第一台 php 服务器,第二秒访问的是第三台服务器。登录的信息一般是保存在 session 中的。这样子登录保存的 session 数据就需要进行共享了。不然的话会出现,访问第一台服务器生成了一个 session 数据。第二秒负载请求到第三台服务器,结果获取不到刚才生成的 session 数据,就会提示用户重新登陆以生成 session 数据。但是假如下一秒负载请求到第二台服务器呢?
因此,这里我们要谈论的问题其实是 session 数据的共享问题,即多台后端服务器(php,Java 等处理服务器,并不是 nginx、apache 等的 WEB 服务器)共用一份 session 数据的问题。
针对多服务器的 session 共享问题,大概有以下几种解决方案:
第一种方案:
通过复制或同步的方式使得 a、b、c 服务器上都具有相同的 session 数据。
这种方案是,使用一些文件同步工具 (Linux 下的 rsync),当 a 服务器中的 session 数据有更改的时候,就会把这些更改也同步到 b,c 服务器上去。通过复制的方式,最终 a,b,c 各个服务器上都拷贝了一份 session 数据。
这种方式的弊端是,速度慢。复制数据会出现延迟。比如第一秒访问是 a 服务器,修改了 session 数据,负载均衡,可能下一秒访问是 b 服务器,session 数据如果没有被复制到 b 服务器,则是读取不到 session 数据的,出现时间上的延迟。这种复制数据要消耗很多网络带宽的。在实际中业界用得比较少。机器的数量越多,复制数据的性能损耗越大。不具备高度扩展性。
复制 session 的方式,无论是网络带宽成本还是硬件开销上都很大的。
第二种方案:
把原来存储在服务器磁盘上的 session 数据存储到客户端的 cookie 中去。
这样子,就不需要涉及到数据共享了。当客户端请求的时候,原来生成在服务器的数据生成到浏览器的 cookie 中,根据 cookie 中的数据识别用户。php 由原来的”从本地 (也就是服务器) 磁盘上读取 session 数据”转变为”浏览器的 cookie 中读取数据”。
这样子,在多台 php 服务器负载均衡的情况下,即便第一秒请求是 a 服务器,第二秒请求是 b 服务器,都不需要管哪台服务器了。反正都是读取客户端上的 cookie 数据。
一般是把 session 数据按照自己定义的加密规则,加密后后存在 cookie 中。
数据保存在 cookie 中这种做法有好处,也有坏处。
好处 是服务器的压力减小了,因为 session 数据不存在服务器磁盘上。根本就不会出现 session 读取不到的问题。
带来的 弊端 是:
网络请求占用很多。每次请求时,客户端都要通过 cookie 发送 session 数据给服务器。
另外,浏览器对 cookie 的大小存在限制。每个浏览器限制是不同的。
所以第一种方案不适合高访问量的情况下,因为高访问量的情况下,每次请求浏览器都要发送 session 数据给服务器。一般一个 cookie 大小 2k 的样子。会占用很多带宽(服务器购买带宽是一个很大费用),成本增高。归纳为带宽性能,速度问题。
存储到 cookie 中去,第二方面是安全问题:把 session 数据放到客户端,一般 session 中存的都是重要性数据(帐号、昵称、用户 id 等),会存在安全问题。
了解到,淘宝以前用过这种方式,把 session 数据存储到 cookie 中,根据 cookie 来识别用户。
第三种方案:
用一种算法 (简单理解为规则),什么机制下 session 是保存在哪台服务器下,那么读取的时候就按照这种规则去读取,就能定位到原来的服务器。叫做 分发请求,分发到特定的服务器上去,我理解其原理是存 session 和读 session 数据保证都在一台服务器操作,就不会需要涉及到共享,具体实现方式是通过约定一种分发机制来实现。
例如 Nginx 的 ip_hash,每个请求按访问 ip 的 hash 结果分配,这样每个访客固定访问一个后端服务器。
假设是同一个用户 user1,每次访问都路由到同一台服务器上, 这样即便是在负载均衡的情况下,也能保证每次访问都能读取到 session,不需要做 session 数据共享了。
这种方式的 弊端 是:
1、如果这台机子挂掉了,那么后续的请求按照 session 的规则还是会分发到这台服务器上去,但是现在不可用了,就会导致归属到这台服务器上的所有用户登录失败。
2、本来负载均衡有一个目的就是:当其中一台机子不可用的时候,会自动分发到可用的机子上去(自动判断现在要请求的机子是否可用)。
第四种方案:
做一个中间层,专门来存储所有访问涉及到的 session。也就是所有的 session 都存储在这里。
服务器端统一从这里读取 session 数据。
比如以下的模式:
对比于前面的几种方式,第四种方式才算是真正的实现了 session 数据的共享。
利用这种中间层的方式共享 session 数据,我们可以通过 NFS 文件共享的方式,多台 php 服务器共享保存 session 文件的磁盘。
通过 nfs 的方式,各个 php 服务器操作 session 数据的时候,是读取本地磁盘目录,但实际上是一个共享网络文件。各个 php 服务器实际上操作的都是同一个目录的文件。
采用中间层的方式来共享 session 数据是不错的,但是正如前面问题二所说的,采用文件的方式来存储 session 数据确实不是一个好办法,我们在问题二的讨论中再说比较好的解决方案。
问题二的讨论:
由于使用文件的方式来存储 session 数据带来的大量 I/O 操作而使服务器的性能下降,我们必须找到解决方案:
方案一:数据库
保存在数据库中,这种方式的扩展性很强,可以随意增加 WEB 而不受影响。放在数据库里面安全方面好。
要使用数据库来存储 session 数据的话,其实就是我们自己按照 session 的机制来模拟实现 session 的存储,我们可以通过 session_set_save_handler() 函数来实现这个想法。具体为,把以前存储在文件中的 session 数据存储到数据库中去,那么这样做,其实就不用到 php 内置的 session 机制了(像 session_start() 之类的函数都不需要去用了)。
写程序要模拟的是,从数据库拿 session 数据,约定什么情况下数据过期了然后自动清理,这里是指删除数据库中的行。保存在文件中的时候,php 有垃圾回收机制会去自动清理过期的 session 文件。
放在数据库里面,访问量小没有问题。大流量网站这么做,只会拖慢速度。因为得查询数据库,造成数据库压力大。假如你的整个 web 程序所使用的数据库和存储 session 数据所使用的数据库是同一个的话,那数据库真的是苦不堪言。但是单独为 session 而分出一台服务器的话,好像又有点浪费资源的感觉。
有些做法跟这种思想是类似的:比如 ecshop、phpcms 是把 session 数据都存储在数据库中去。服务端就是从数据库中拿 session 的数据。
放到数据库存储后,就可以实现:多台 web 服务器统一操作数据库,因为数据都在数据库,web 服务器都能从数据库进行读取,那么 session 数据就能实现共享。
存储在数据库的做法,在线人数决定了其瓶颈,主要问题是影响性能。在线人数, 因为登录的 session 数据存储在数据库中,只要是登录的用户就会涉及到频繁操作数据库。
访问量大的话,一个用户访问了 n 多个页面,哪怕是刷新页面,都需要去数据库取 session 数据。数据库的承受压力,确实很恐怖。pv 是多少,就要请求多少次数据库服务器。访问每个页面都会去数据库查询是否登录,或者添加数据进数据库的 sessions 表
而保存在文件中的时候,则交给了操作系统去控制。一个用户怎么刷新页面,查看其他页面,都只需要读取单个 session 文件(sess_74dd7807n2mfml49a1i12hkc45)。
方案二:缓存实现
可以将 session 数据保存在 memcached,Redis 之类内存数据库中,memcached 是基���内存存储数据的,性能很高,用户并发量很大的时候尤其合适。
主要是利用内存的数据读取速度是很快的,与磁盘读取的速度不是一个数量级的。
使用内存存储:方便统计在线人数,内存的速度比磁盘访问快、内存数据库系统能够控制内存中的过期数据自动失效(刚好符合 session 过期需要)。
存储在 redis 比较理想的选择,存储在数据库中方便存储统计在线人数,那么存储在 redis 中也实现了这个要求。
也可以存储在 memcache 中。但 redis 支持的数据类型多。所以用它好点。
总结:
1、针对负载均衡来说,使用中间层的方式是最为理想的
2、关于取缔使用文件存储 session 的问题,是使用数据库存储还是使用缓存存储是要根据 web 程序或者开发者自身的情况有关的,如果 web 程序的规模不是很大,出于经济成本考虑而使用虚拟主机或共享主机的话,由于不具备对服务器的完全控制权限,比如还要安个 memcache 之类的, 修改 php.ini 之类的都需要自己拥有独立服务器才能操控的,推荐使用数据库来存储。相反,如果你的 web 程序规模不小,网站很活跃,而且能够拥有独立的服务器的话,使用缓存方式将会得到理想的效果。
3、在后续的文章中,我将实现使用数据库存储 session 数据和使用缓存存储 session 数据。
前言:
在不久之前,本人去参加了某公司的实习面试,其中 HR 问我关于 SESSION 实现的原理,当时我就懵逼了,因为在之前的开发中,我只知道 session 与 cookie 的区别在于:session 是保存在服务器端,cookie 保存在客户端。那 session 在服务端是怎么样保存的?session_id 又是什么?等等。我当时答不上来。回来后决定把这些搞懂。
为什么要使用 SESSION?
是因为目前网络中所使用的 http 协议造成的,http 协议是无状态协议,通俗点说就是当你发送一次请求道服务器端,然后再次发送请求到服务器端,服务器是不知道你的这一次请求和上一次请求是来源于同一个人发送的。而 session 就能很好解决这个问题。
在我们的访问期间,各个页面间共享的数据放在 session 中,就比如说我们的登陆信息,如果没有 session 的话,当你在这个页面登陆之后,在点击下一个页面的时候你需要再次登陆。
引入:
现在我们来看看平时我们是怎么使用 session 的,大家看下面的例子:
<?php
#index.php 文件
session_start(); // 启动会话
$user = isset($_GET['user'])?$_GET['user']:"default"
if(!isset($_SESSION['user'])){$_SESSION['user'] = $user; // 设置会话
}
var_dump($_SESSION);
unset($_SESSION['user']); // 清除会话
现在我们在浏览器 A 打开 http://localhost/index.php?user=lsgogroup;
返回:array(1){["user"]=>string(9)"lsgogroup"}
在浏览器 B 打开 http://localhost/index.php
返回:array(1){["user"]=>string(7)"default"}
问题:
- session_start() 的作用是什么?
- 为什么在浏览器 B 中返回的不是:array(1){[“user”]=>string(9)”lsgogroup”}?
- $_SESSION 数组是怎么保存这些数据的?
理解 PHP SESSION 机制:
session 机制是一种服务器端的机制,服务器使用一种类似于散列表的结构来保存信息。
当程序需要为某个客户端的请求创建一个 session 的时候,服务器首先检查这个客户端的请求(Http Request)里是否已包含了一个 session 标识 - 称为 sessionid,如果已包含一个 sessionid 则说明以前已经为此客户端创建过 session,服务器就按照 sessionid 把这个 session 检索出来使用,如果客户端请求不包含 sessionid,则为此客户端创建一个 session 并且生成一个与此 session 相关联的 sessionid,sessionid 的值应该是一个既不会重复,又不容易被找到规律以仿造的字符串,这个 sessionid 将被在本次响应中返回给客户端保存。而这个 sessionid 就是作为客户端的唯一标识而存在的(即使在同一台电脑上,浏览器 A 和浏览器 B 对于服务器来说都是不同的客户端)。
上面一段话你可能暂时不会理解,不过不要紧,我会在下面作出解释:
现在我们来看看浏览器 A 和 浏览器 B 的 cookie:
浏览器 A(这里对应是谷歌浏览器):
浏览器 B(这里对应是火狐浏览器):
对比可以看到,两个浏览器对于 localhost 都有一条名为 PHPSESSID 的 cookie 记录,而这个 PHPSESSID 就是上面所说的 sessionid,它告诉服务器请求是来自浏览器 A 还是浏览器 B。
现在我们可以回答上面的问题 2 了:
由于浏览器 A 的 PHPSESSID 和浏览器 B 的 PHPSESSID 是不一样的,因此服务器根据 sessionid 检索 session 的数据也是不一样的,也就是说浏览器 A 请求的 $_SESSION 数组和 浏览器 B 请求的 $_SESSION 数组也是不一样的。
(当然,PHPSESSID 这个 id 名不是固定的,我们可以在 PHP.ini 文件中的 session.name 项进行修改。)
上面的例子是使用 COOKIE 保存 PHPSESSID,但是,由于 cookie 可以被人为的禁止,必须有其他机制以便在 cookie 被禁止时仍然能够把 sessionid 传递回服务器。有两种技术可以解决这个问题:
- URL 重写,就是把 sessionid 直接附加在 URL 路径的后面:http://localhost/index.php?user=lsgogroup&PHPSESSID=ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng
- 隐藏表单传递。
由于这不是重点,这里不展开讲。
SESSION 是怎么存储数据的?
答:session 是以文件的形式保存的。
php.ini 中的配置项 session.save_handler = files;
默认为 file,定义 session 在服务端的保存方式,file 意为把 session 保存到一个临时文件里。
php.ini 中的配置项 session.save_path=“”;
这个里面填写的路径,将会使 session 文件保存在该路径下。
session 文件的命名格式是:”sess_[PHPSESSID 的值]”。每一个文件,里面保存了一个会话的数据。
我们查看服务器端 session.save_path 目录会发现很多类似 sess_vv9lpgf0nmkurgvkba1vbvj915 这样的文件,这个其实就是 sessionid(也就是 PHPSESSID)“vv9lpgf0nmkurgvkba1vbvj915″ 对应的数据。真相就在这里,客户端将 sessionid 传递到服务器,服务器根据 sessionid 找到对应的文件,读取的时候对文件内容进行反序列化就得到 session 的值($_SESSION 数组中的数据),保存的时候先序列化再写入。
由于我做实验的时候使用的是 Ubuntu 系统,因此我的 session.save_path 默认实在 /var/lib/php/sessions 下,我们来看看前面浏览器 A 生成的 session 文件是怎样的(浏览器 A 的 PHPSESSID =‘nqqleletmsb0nuf7d4ulvotk45’):
cd /var/lib/php/sessions
# 由于 session 数据是很重要的数据,因此必须只能 root 用户才能打开
sudo vim sess_nqqleletmsb0nuf7d4ulvotk45
# 看看文件格式是不是 "sess_[PHPSESSID 的值]"
文件内容:
user|s:9:"lsgogroup";
从文件内容可以看到,数据是经过序列化的,数据的读取规则是这样的:
- 每一个 session 的值是以分号”;”分开的。比如”user|s:9:”lsgogroup”;“就是一个完整的 session 值结束,如果再添加 $_SESSION[‘name’]=”LSGOZJ”,则变成这样”user|s:9:”lsgogroup”;name|s:7:”LSGOZJ”;“
- 里面的读取规则:符号“|”前面表示 session 名称。符号后面是该 session 的具体信息。包括:数据类型,字符长度,内容。比如说”user|s:9:”lsgogroup”;“,$_SESSION[‘user’] 的值是“lsgogroup”,是一个长度为 9 的字符串。
- 等等。。。
到了这里,我们就解决了上面的问题 3 了。
其实还有很多种存储 session 的方式,如果我们想自定义别的方式保存(比如用数据库),则需要把该项设置为 user;我们还可以使用 memcache、Redis 等优秀的缓存系统(前提是你的服务器安装了此类软件)。
session_start()函数的作用是什么?
了解的原理之后,所谓的 session 其实就是客户端一个 sessionid 对应服务器端一个 session file,新建 session 之前执行 session_start() 是告诉服务器要种一个 cookie 以及准备好 session 文件,要不然你的 session 内容怎么存;读取 session 之前执行 session_start() 是告诉服务器,赶紧根据 sessionid 把对应的 session 文件反序列化。
说白了,当我们使用 php 的内置函数 session_start() 的时候,就是到服务器的指定的磁盘目录把 session 数据载入,实际上就是拿类似 sess_74dd7807n2mfml49a1i12hkc45 的文件。
只有一个 session 函数可以在 session_start() 之前执行,session_name():读取或指定 session 名称(比如默认的就是”PHPSESSID”),这个当然要在 session_start 之前执行。
根据 http 的请求机制,当浏览器请求的时候,头部信息会把浏览器中的 cookie 一起发给服务器。PHPSESSID 这个 cookie 也是在其中发给了服务器,php 引擎通过读取 PHPSESSID 的值来确定要载入哪个 session 文件。
比如值为 74dd7807n2mfml49a1i12hkc45,载入的就是”sess_74dd7807n2mfml49a1i12hkc45”。
注:当你调用 php 的函数 session_start(), 才表明你需要使用 session 文件了。不然平白无故就去载入文件,浪费性能。
SESSION 的清理:
在平时我们谈论 SESSION 的机制的时候,常常听到这样一种误解“只要关闭浏览器,session 就消失了”(本人也是一度认为这样),其实可以想象一下会员卡的例子,除非顾客主动对店家提出销卡,否则店家绝对不会轻易删除顾客的资料。
对 session 来说也是一样的,除非程序通知服务器删除一个 session,否则服务器会一直保留,程序一般都是在用户做 logoff(注销操作,类似于 session_destroy()操作)的时候发个指令去删除 session。然而浏览器从来不会主动在关闭之前通知服务器它将要关闭,因此服务器根本不会有机会知道浏览器已经关闭,之所以会有这种错觉,是大部分 session 机制都使用会话 cookie 来保存 sessionid,而关闭浏览器后这个 sessionid 就消失了,再次连接服务器时也就无法找到原来的 session,但是服务器上对应的 session file 依然存在。
为什么关闭浏览器后 sessionid 就会消失呢?这跟 cookie 在客户端的存储有关,如果在设置 cookie 的时候没有指定生命周期,那么 cookie 的数据是存储在内存中的,当浏览器被关闭,内存被回收了,那么 cookie 也就没有了(这就是为什么 cookie 在没有指定生命周期的时候,其生命周期与浏览器生命周期一样)。
如果服务器设置的 cookie 被保存到硬盘上(设置了生命周期),或者使用某种手段改写浏览器发出的 HTTP 请求头,把原来的 sessionid 发送给服务器,则再次打开浏览器仍然能够找到原来的 session。
恰恰是由于关闭浏览器不会导致 session 被删除,迫使服务器为 seesion 设置了一个失效时间,当距离客户端下一次使用 session 的时间超过这个失效时间时,服务器就可以认为客户端已经停止了活动,才会把 session 删除以节省存储空间。
我们来看看服务器是怎样删除 session 数据的:
session.gc_probability = 1
session.gc_divisor = 100
session.gc_maxlifetime = 1440
这三个配置项组合构建服务端 session 的垃圾回收机制。
session.gc_probability 与 session.gc_divisor 构成执行 session 清理的概率,理论上的解释为服务端定期有一定的概率调用 gc(garbage collection 垃圾回收)进程来对 session 进行清理,清理的概率为:gc_probability/gc_divisor 比如:1/100 表示每一个新会话初始化时,有 1% 的概率会启动垃圾回收程序,清理的标准为 session.gc_maxlifetime 定义的时间(清理过期的数据)。
我所用的系统是 ubuntu,php.ini 中指定的 session.gc_probability = 0,也就是概率为零,原因是该系统是使用 cron 脚本来执行垃圾清理的。
后话:
session 还有很多需要整理和学习的地方,如:
- session 多服务器共享的问题,假如有多台 php 服务器进行负载均衡的时候,用户登录时访问的是第一台服务器,没准下一个页面访问的是第二台服务器,但是 session 数据是存储在第一台服务器上的,因此在访问下一个页面的时候由于没有 session 数据(第二台服务器上)导致用户必须重新登陆。
- 从上面的分析我们也知道,php 中 session 默认通过文件的方式实现,但是如果访问量大,可能产生的 SESSION 文件会比较多,从众多的文件中选择其中一个文件不是一件轻松的事情,而且每次都以打开文件、读取文件的方式,也会产生大量的 I/O 操作,严重影响服务器的性能。
本文参考了很多网上的资源,加上自己的理解,由于篇幅已经很长,因此上面所提到的问题我会在后续文中解决。
更多详情见请继续阅读下一页的精彩内容:http://www.linuxidc.com/Linux/2017-05/143594p2.htm
前言:
本篇文章是继承自我的前面的两篇文章《对 PHP SESSION 的深入理解(一)》、《对 PHP SESSION 的深入理解(二)》而来的,主要是解决前面的问题。
为什么要使用数据库保存 session 数据?
就 PHP 来说,语言本身支持的 session 是以文件的形式保存在磁盘文件中,保存在指定的文件夹中,保存的路径可以在配置文件 php.ini 中设置。但是按照默认的存储方法是有很大的弊端:
- 保存到文件系统中,只要用到 session 就会从好多个文件中查找是定的 sessionId 对应文件,效率很低,而且导致的 I/O 操作很多;
-
当用到多台服务器做负载均衡的时候,出现 session 丢失问题(其实是保存在了其他服务器上)。
使用数据库来存储 session 数据,我们就能解决上面的问题。
在之前我曾经写过一篇关于这个主题的文章《PHP 数据库保存 session 会话》,为了和现在我这个小系列“对 PHP SESSION 的深刻理解”保持统一,我就再写一次,当然,通过最近的学习,懂得东西肯定比以前多了。
第一步:建数据库
1、创建会话表
由于 session 数据是保存在服务器上面的,而在客户端中保存的是一个索引(sessionID), 这个索引对应于服务器上的某一条 session 数据。因此该表必须包含的两个字段是 id、data,还有就是会话会有过期时间,所以在这里还有个字段就是 last_accessed,这里我把该表建在 test 数据库下:
CREATE TABLE sessions(id CHAR(32) NOT NULL,
data TEXT,
last_accessed TIMESTAMP NOT NULL,
PRIMARY KEY(id)
);
PS:如果程序需要在会话保存大量的数据,则 data 字段可能就需要定义为 MEDIUMTEXT 或 LONGTEXT 类型了。
2、创建针对 session 的数据库用户
# 创建用户
CREATE USER sess_user IDENTIFIED BY "sess_pwd";
#授权访问
GRANT SELECT,UPDATE,INSERT,DELETE ON test.sessions TO sess_user;
现在数据库已经有了,接下来呢就是代码实现 session 数据的存储了。
第二步:编写会话函数
1、修改配置文件,告诉 php 引擎使用我们自己的 session 处理函数
打开 php.ini 配置文件,将
session.save_handler = files
改成:
session.save_handler = user
重启服务器
2、通过 php 提供的接口,自己改写 session 的处理函数
要想实现自定义地处理 session,关键是通过调用函数 session_set_save_handler()来完成的。
php5.4 及之后可以直接实现 SessionHandlerInterface 接口,代码会更加简洁。该接口的结构如下:
SessionHandlerInterface {/* 方法 */
abstract public bool close (void )
abstract public bool destroy (string $session_id )
abstract public bool gc (int $maxlifetime )
abstract public bool open (string $save_path , string $session_name )
abstract public string read (string $session_id )
abstract public bool write (string $session_id , string $session_data )
}
我们新建 session.inc.php,代码如下:
<?php
/**
* Created by PhpStorm.
* User: lsgozj
* File: session.inc.php
* Desc: 处理 session 的自定义类
* Date: 16-12-10
* Time: 下午 4:39
*/
class mysqlSession implements SessionHandlerInterface
{
private $_pdo = null; // 数据库链接句柄
// 这些信息应该放在配置文件中。。。。
private $_configs = array('dbms' => 'mysql', // 数据库类型
'dbhost' => 'localhost', // 主机
'dbname' => 'test', // 数据库名
'dbtable' => 'sessions', // 数据库表
'dbuser' => 'sess_user', // 用户
'dbpwd' => 'sess_pwd', // 密码
);
// 自定义 session_start()函数
public static function my_session_start()
{
$sess = new self;
session_set_save_handler($sess); // 注册自定义函数,在 php5.4 之后,session_set_save_handler()参数直接传 SessionHandlerInterface 类型的对象即可。
session_start();}
/**
* session_start() 开始会话后第一个调用的函数,类似于构造函数的作用
* @param string $save_path 默认的保存路径
* @param string $session_name 默认的参数名(PHPSESSID)* @return bool
*/
public function open($save_path, $session_name)
{
$dsn = $this->_configs['dbms'] . ":host=" . $this->_configs['dbhost'] . ";dbname=" . $this->_configs['dbname'];
try {$this->_pdo = new PDO($dsn, $this->_configs['dbuser'], $this->_configs['dbpwd']);
return true;
} catch (PDOException $e) {return false;
}
}
/**
* 类似于析构函数,在 write()之后调用或者 session_write_close()函数之调用
* @return bool
*/
public function close()
{
$this->_pdo = null;
return true;
}
/**
* 读取 session 信息
* @param string $sessionId 通过该 ID(客户端的 PHPSESSID)唯一确定对应的 session 数据
* @return session 信息或者空串(没有存储 session 信息)*/
public function read($sessionId)
{
try {$sql = 'SELECT * FROM' . $this->_configs['dbtable'] . 'WHERE id = ? LIMIT 1';
$res = $this->_pdo->prepare($sql);
$res->execute(array($sessionId));
if ($ret = $res->fetch(PDO::FETCH_ASSOC)) {return $ret['data'];
} else {return '';
}
} catch (PDOException $e) {return '';
}
}
/**
* 写入或修改 session 数据
* @param string $sessionId 要写入数据的 session 对应的 id(PHPSESSID)* @param string $sessionData 要写入的是数据,已经序列化过的
* @return bool
*/
public function write($sessionId, $sessionData)
{
try {$sql = 'REPLACE INTO' . $this->_configs['dbtable'] . '(id,data) VALUES(?,?)';
$res = $this->_pdo->prepare($sql);
$res->execute(array($sessionId, $sessionData));
return true;
} catch (PDOException $e) {return false;
}
}
/**
* 主动销毁 session 会话
* @param string $sessionId 要销毁的会话的唯一 ID
* @return bool
*/
public function destroy($sessionId)
{
try {$sql = 'DELETE FROM' . $this->_configs['dbtable'] . 'WHERE id = ?';
$res = $this->_pdo->prepare($sql);
$res->execute(array($sessionId));
return true;
} catch (PDOException $e) {return false;
}
}
/**
* 清理会话中的过期数据
* @param int $maxlifetime 有效期(自动读取配置文件 php.ini 中的 session.gc_maxlifetime 配置项)* @return bool
*/
public function gc($maxlifetime)
{
try {$sql = 'DELETE FROM' . $this->_configs['dbtable'] . 'WHERE DATE_ADD(last_accessed,INTERVAL ? SECOND) < NOW()';
$res = $this->_pdo->prepare($sql);
$res->execute(array($maxlifetime));
return true;
} catch (PDOException $e) {return false;
}
}
}
到了这一步我们的任务基本上是完成了,现在我们来测试一下是否可用:
# test.php 文件
<?php
require_once('./session.inc.php');
mysqlSession::my_session_start(); // 开启会话
$_SESSION['name'] = 'LSGOZJ';
$_SESSION['age'] = 22;
var_dump($_SESSION);
在浏览器访问 test.php,然后去数据库里看看,是否已经成功插入数据库:
你可以在另一个 php 文件里面看看是否能够读取:
# test1.php
<?php
require_once('./session.inc.php');
mysqlSession::my_session_start(); // 开启会话
echo $_SESSION['name'];
如果发现不能读取的话,就得检查上面的步骤了。
大家可能会发现,在整个过程中我都没有对表中的 last_accessed 字段进行操作,因为这个字段是 timestamp 类型的,它会在表更新和插入时默认插入当前时间,因此我们其实不用管该字段。
第三步:谈谈 session 清理
本人在完成上面的所有步骤之后,一度怀疑过过期的 session 数据系统会帮我清除吗?
我的环境:
Ubuntu:16.04
Php:7.0
我在我的第一篇文章《对 PHP SESSION 的深入理解(一)》中对 session 的清理有过分析,在这里在给大家复习复习:
配置文件 php.ini 中有如下三个配置项:
session.gc_maxlifetime
session.gc_probability
session.gc_divisor
这三个配置项的组合构建服务端 session 的垃圾回收机制。
session.gc_probability 和 session.gc_divisor 构成在每个会话初始化时启动 gc(garbage collection 垃圾回收)进程的概率,此概率用 gc_probability/gc_divisor 计算得来。例如 1/100 意味着在每个请求中有 1% 的概率启动 gc 进程。而清理的标准为 session.gc_maxlifetime 定义的时间。
例如:
session.gc_maxlifetime = 1440 表示当 session 数据在 1440s 后还没有被访问的话,则该 session 数据将会被视为“垃圾数据”,并且等待 gc(垃圾回收)进程的调用的时候被清理掉。
注意:一般对于一些大型的门户网站,建议将 session.gc_divisor 调大一点,减少开销。
那么我的问题是什么呢?因为在我的环境下,php.ini 中指定的 session.gc_probability = 0,也就是说启动 gc 进程的概率为零。前面我也说了,概率为零是因为系统默认不使用 gc 进程,而是使用 cron 脚本来执行垃圾清理的。
既然系统不使用 gc 进程,那是不是说明上述代码中的 gc 函数就永远得不到执行了?带着这个疑问,我做了个实验:
分别使用上面定义的方法和 php 原来的方法生成一些 session 数据,然后在一段时间后(超过 session.gc_maxlifetime), 去检查数据库中的 session 数据,发现数据还在,而 /var/lib/php/sessions 下的 session 文件已经被清理掉了!当然有可能是概率的问题,后来我又试了几次,发现结果还是一样!
而当我将 php.ini 中指定的 session.gc_probability 改为大于 0 的数之后,发现数据库中的过期的数据被清除掉了。
因此,大家在使用数据库存储 session 数据的时候一定要注意修改 session.gc_probability 配置项。
总结:
1、通过这个例子,对 session 机制的理解更加深
2、复习了一遍 PDO 操作(离上一次使用有点久)
本文永久更新链接地址:http://www.linuxidc.com/Linux/2017-05/143594.htm