PHP 维持状态

本文最后更新于:2024年3月18日 凌晨

PHP 维持状态

  • HTTP是一个无状态的通讯协议,这意味着一旦Web服务器完成了客户端的Web页面请求,它们之间的连接也就断开了,换句话说,我们没法使服务器识别来自于同一个客户端的一系列请求。
  • 但是状态是很有用的,比如,如果不能跟踪来自于同一个用户的一系列请求,就无法设计一个购物车程序,你需要知道什么时候一个用户在购物车中放入物品,所以时候删除,当他想结账的时候购物车中有些什么物品。
  • 为了解决Web状态不维持的问题,程序员提出了不少在两次请求间跟踪状态信息的方法(这称为会话跟踪,session tracking),其中的一种方法是使用隐藏的表单字段来传递信息,因为PHP以对待正常表单字段的方式来对待隐藏表单字段,所以可以通过_GET$_POST数组范文隐藏表单字段的值,这样利用隐藏表单字段,就可以传递购物车的全部信息。
  • 另一种更常用的方法是给每一个访问的用户分配一个唯一标识符,并且通过一个隐藏表单字段传送ID,隐藏表单字段能在所有的浏览器使用,但它们只能用于一系列动态创建的表单,所以它们不如其他的技术那么通用(这种方法不太好用,因为表单提交的下一个页面也必须含有表单,在多个页面间传递信息,则多个页面都需要表单,实现起来比较繁琐,实际应用中常用于"向导型"页面,如注册程序,通过"下一步"按钮,用户跳到下个页面继续注册,可在下个页面保持上一页中用户输入的信息)
  • 另一个方法是URL重写,用户访问的每一个URL都加上附加的信息,附加信息作为URL的参数,例如,如果你为每个用户分配一个唯一的ID,你可以在每个URL中包含该ID,如下所示:
1
http://www.example.com/catalog.php?userid=123
  • 如果你确定要动态修改所有的本地链接以包含一个用户ID,那么你可以在程序中跟踪单个用户的信息,URL重写可用于所有动态生成的文档,而不仅是包含表单的文档,但是很显然为每个页面重写是非常乏味的工作。
  • 实现状态维持的第三种方法是使用Cookie,Cookie是服务器给客户端的一些信息,在后续的请求中,客户端将把信息返回给服务器,这样就可以标识自己的身份,Cookie对于浏览器重复访问时保持客户端信息很有用,但它也有不足的地方,主要问题是有些浏览器不支持cookie,即使支持,用户也可以禁用cookie,所以使用cookie来维持状态的程序需要使用另一个叫回退(fallback)的机制,我们会在后面再详细讨论cookie
  • 在PHP中维持状态的最好的方法是使用其内置的会话跟踪系统(session-tracking system),该系统允许你创建一个持久型变量,从程序的不同页面和同一用户对站点的不同访问都可以访问该变量,PHP的会话跟踪机制使用cookie(或URL)在后台优雅地解决了维持状态的大部分问题,并为你考虑了所有细节。

Cookies

  • Cookie是一个包含多个字段的字符串,一个服务器可发送包含在响应头中的一个或多个cookie给浏览器,Cookie中的一些字段指明了那些需要浏览器将cookie发送至的页面,Value字段是cookie中的有效内容----服务器能够将一些它所需要的数据保存在那儿(有限制),诸如用于标识用户唯一ID,首选项(preference)等。
  • 可以使用setcookie()函数来向浏览器发送cookie:
1
setcookie(name [, value [, expire [, path [, domain [, secure]]]]]);
  • 该函数利用利用给定的参数创建cookie串,必须以该串为值创建一个cookie头,因为cookie是作为头发送的,所以必须在文件体被传递之前调用setcookie()函数,setcookie()函数的参数包括:

    • name:为某一特定的cookie所取的唯一名称,你可以让多个cookie拥有不同的名称和属性,名称中不能有空格和分号。
    • value:Cookie所带的任意字符串值,老版本的Netscape限制了一个cookie的大小不能大于4KB(包括名称,过期时间和其他信息),所以当没有关于cookie的明确限制时,最好不要让cookie大于3.5KB
    • expire:在cookie的过期时间,如果没有指定过期时间,浏览器将把此cookie保存在内存中而不是硬盘上,当浏览器退出页面访问时,cookie也就消失了,过期时间是以格林尼治标准时间1970年1月1日为开始时间的,单位是秒,例如,传送time()+60*60*2将使cookie在2小时后过期。
    • path:只有对该路径下的URL,浏览器才会返回cookie,默认路径为当前页面所在的目录,例如,如果/store/front/cart.php设置了cookie,且没有指定路径,那么cookie将被浏览器返回给服务器上所有/store/front目录下的页面。
    • domain:只能对此域中的URL,浏览器才会为其返回cookie
    • secure:浏览器只通过https连接上传cookie,它的默认值为false,其含义是通过不可靠的连接(即普通的http连接,而非https安全连接)发送cookie
  • 当浏览器发送一个cookie给服务器时,你可以用$_COOKIE数组来访问,键名为cookie的名称,值为cookie的value字段。

1
2
3
4
<?php
$page_accesses = $_COOKIE['accesses'];
setcookie('accesses',++$page_accesses);
?>
  • 当对cookie进行解码时,任何cookie名称中的句点(.)将被转换为下划线(_),例如一个名为tip.top的cookie可以通过$_COOKIE[tip_top']来访问。
  • 下例展示了一个HTML页面,它可以然你选择网页的前景色和背景色:

示例7-10:首选项选择。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<html>
<head><title>Set Your Preferences</title></head>
<body>
<form action="prefs.php" method="post">
Background:
<select name="background">
<option value="black">Black</option>
<option value="while">White</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
</select><br/>

Foreground:
<select name="background">
<option value="black">Black</option>
<option value="while">White</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
</select><br/>

<input type="submit" value="Change Preferences">
</form>
</body>
</html>
  • 上例中的表单提交到PHP脚本prefs.php(即示例7-11),这段脚本为表单中的颜色首选项设置cookie,注意对setcookie()的调用是在HTML页面开始之前完成的。

示例7-11:用cookie来设置首选项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
$color = array('black' => '#000000',
'white' => '#ffffff',
'red' => '#ff0000',
'blue' => '#0000ff');

$bg_name = $_POST['background'];
$fg_name = $_POST['foreground'];

setcookie('bg',$color[$bg_name]);
setcookie('fg',$color[$fg_name]);
?>
<html>
<head><title>Preferences Set</title></head>
<body>
Thank you. Your preferences have been changed to:<br/>
Background:<?= $bg_name ?><br/>
Foreground:<?= $fg_name ?><br/>

Click <a href="prefs-demo.php">here</a>to see the preferences in action.
</body>
</html>
  • 上例生成的页面包含一个到另一页面(见示例7-12)的超链接,这个页面访问$_COOKIE数组并使用其中的颜色首选项。

示例7-12:通过cookie应用颜色首选项。

1
2
3
4
5
6
7
8
9
10
11
12
13
<html>
<head><title>Front Door</title></head>
<?php
$bg = $_COOKIE['bg'];
$fg = $_COOKIE['fg'];
?>
<body bgcolor="<?= $bg ?>" text="<?= $fg ?>">
<h1>Welcome to the Store</h1>

<p>We have many fine products for you to view. Please feel free to browse the aisles and stop an assistant at ant time. But remember, you break it you bough it!</p>
Would you like to <a href="prefs.html">change your preferences?</a>
</body>
</html>
  • 使用cookie有很多需要注意的地方,不是所有客户端支持和接收cookie,即使支持,用户也可能禁用cookie功能。
  • cookie规范规定了cookie不能大于4KB,一个域只能发送20个cookie,而且一个客户端只能存储300个cookie,有的浏览器可能限制宽松一些,但你不能依赖于此。
  • 无法控制客户端浏览器使cookie过期,当浏览器有能力并且需要新增一个cookie时,它将抛弃一个没有过期的cookie,你在使cookie短时间内过期时小心,过期时间取决于客户端机器的时钟,它未必和服务器时钟一样准,有很多用户的机器时间是不准的,所以你不能依赖于快速过期。
  • 尽管有这么多限制,cookie仍不失为一种在浏览器重复访问时保持信息的使用方法。

会话

  • PHP内置了对session的支持,以替你处理所有的cookie操作,并提供不同页面和多次访问都可用的持久性变量,Session让你可以容易地创建多页面的表单(如购物车),在页面到页面之间保存用户鉴别信息以及保存永久用户在某一站点上的首选项。
  • 每个第一次访问的用户都会被分配一个唯一的与别人的sessionID,默认地,seddionID存放在一个名为PHPSESSID的cookie中,如果用户的浏览器不支持cookie或者禁用了cookie,sessionID会被传递到WEB站点的URL内。
  • 每个session都有一个关联的数据存储,你可以在页面开始时注册(register)来自于数据存储的变量,并在页面结束时将其保存回数据存储,注册的变量在多个页面之间都可用,且一个页面内对变量的改动在其他页面也是可见的(也就是说变量的作用域跨越了多个页面)
  • 例如"add this to your shopping cart”链接会把用户带到另一个页面,该页面将向购物车的物品数组中添加一项,该数组可以被其他页面访问,用于显示购物车中的内容。
  • 为了在一个页面中启用session功能,可在文档生成之前调用session_start()函数:
1
2
3
4
<?php session_start() ?>
<html>
...
</html>
  • 如果有必要,以上代码会分配一个新的sessionID(可能会创建一个要发送到浏览器的cookie),并从数据存储中载入持久性变量。
  • 如果你注册了对象,这些对象的类定义必须在session_start()调用之前被载入。
  • 传递一个变量名给$_SESSION[]数组可以注册一个session变量,例如,以下是一个点击计数器:
1
2
3
4
5
<?php
session_start();
$_SESSION['hits'] = $_SESSION['hits'] + 1;
?>
This page has been viewed<?= $_SESSION['hits'] ?>times.
  • session_start()函数将已经注册的session变量载入到关联数组$_SESSION,键名为变量名称(如$_SESSION[‘hits’])
  • 你可以用session_unregister()来从$_SESSION数组中删除一个已经注册的变量,同时也其从数据存储中删除,如果给定变量已注册,则函数session_is_registered()返回ture,session_id()函数返回当前session的ID
  • 可以用session_destroy()结束一个session,它将从当前session的数据存储中删除该session,但不会删除浏览器缓存中的cookie,这也意味着,在对启用了session的页面的后续访问中,在调用session_destroy()之前,用户将拥有相同的的sessionID,但没有数据。
  • 下例改写了示例7-11的第一个代码块,它使用了session来代替手动设置的cookie

示例7-13:用session来设置首选项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$colors = array('black' => '#000000',
'white' => '#ffffff',
'red' => '#ff0000',
'blue' => '#0000ff');
session_start();
session_register('bg');
session_register('fg');

$bg_name = $_POST['background'];
$fg_name = $_POST['foreground'];

$bg = $colors[$bg_name];
$fg = $colors[$fg_name];
  • 下例是示例7-12的改进,它使用了session,一旦创建了会话,也就同时创建了变量$bg和$fg,脚本所要做的只是使用它们。

示例7-14:在页面上应用session中设置的首选项。

1
2
3
4
5
6
7
8
9
10
11
12
<?php
session_start()
?>
<html>
<head><title>Front Door</title></head>
<body bgcolor="<?= $bgcolor ?>">
<body bgcolor="<?= $bg ?>" text="<?= $fg ?>">
<h1>Welcome to the Store</h1>
Would you like to <a href="prefs.html">change your preferences?</a>
</body>
</body>
</html>
  • 默认地,保存sessionID的cookie会在浏览器关闭时过期,也就是说,session在浏览器结束访问后不会持久存在,为了改变这个情况,你需要设置php.ini中的session.cookie_lifetime,其值为cookie的生命周期,以秒为单位。

代替cookie传递sessionID的方法

  • 默认情况下,sessionID通过PHPSSESSID cookie在页面间传递,然而PHP的seeson系统支持另两种替代选择:表单字段和URL,通过隐藏表单来传递sessionID是很笨的作法,因为他要求你的每个页面链接都做成一个表单提交按钮,我们将不再讨论这种方法。
  • 通过URL传递sessionID则很优雅,PHP可以重写你的HTML文件,为每个相对链接加上sessionID,这样虽然可行,但是PHP再编译时需要加上-enable-trans-id选项,这样会带来性能上的不良影响,因为PHP不得不解析和重写每个页面,繁忙的站点倾向于使用cookie,因为他们不希望让页面重写导致站点变慢。

自定义存储

  • 默认地,PHP把session信息存储再服务器的临时文件夹中,每个session变量被存在不同的文件里,每个变量都被序列化成适当的格式,你可以编辑php.ini文件来改变设置。
  • 可以通过改变php.ini中session.save_path选项的值来改变session文件的存放位置,如果你在一个共享的服务器上使用你自己安装的PHP,可以把存放目录改为你有权限的目录,这样服务器上其他用户就无法访问你的session文件。
  • 在当前的session存储中,PHP能以两种方式存储session信息,PHP内置格式和WDDX,可以通过设置php.ini中的session_set_save_handler()来注册我们自己写的函数。
1
session_set_save_handler(open_fn,close_fn,red_fn,write_fn,destroy_fn,gc_fn);
  • 可以通过设置httpd.conf文件中的下列选项,将所有的PHP文件放置在自定义的目录下,并使用自定义的session存储:
1
2
3
4
5
<Directory "/var/html/test">
php_value session.save_handler user
php_value session.save_path mydb
php_value session.name session_store
</Directory>
  • mydb值要用包含该数据表的数据库名来代替,自定义session存储用它来寻找数据库。
  • 下面的代码使用MySQL数据库来存储session
  • 数据表的结构如下:
1
2
3
4
5
CREATE TABLE session_store{
session_id char(32) not null PRIMARY KEY,
expiration timestamp,
value text not null
);
  • 第一次需要提供的函数是session打开处理函数,它负责打开一个新的session,它用phpono中的session.save_path的值和包含sessionID的变量名(默认PHPSESSID,可通过php.ono中的session.name来改变)作为参数进行调用。
  • 我们打开函数只是连接数据库,并用存储session信息的数据表名来给全局变量$table赋值。
1
2
3
4
5
6
7
function open($save_path,$session_name){
global $table;
mysql_connect('localhost');
mysql_select_db($save_path);
$table = $session_name;
return true;
}
  • 一旦打开了会话,读/写函数就会在需要的时候被调用来获取和写入当前状态信息,sessionID会作为参数传递给读处理函数,写处理函数,写处理函数则需要两个参数–sessionID和要写入的数据,以下数据库读写函数对数据表进行查询和更新操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function read($session_id){
global $table;
$result = mysql_query("SELECT value FROM $table WHERE session_id='$session_id'");
if($result && mysql_num_rows($result)){
return mysql_result($result,0);
}else{
error_log("read: " . mysql_error() . "\n",3,"/tmp/errors.log");
return "";
}
}
function write($session_id,$data){
global $table;
$data = addslashes($data);
mysql_query("REPLACE INTO $table (session_id,value) VALUES('$session_id','$data')") or error_log("write: ".mysql_error()."\n",3,"/tmp/errors.log");
return true;
}
  • 对应打开函数的是关闭函数,它将在每个页面的代码执行完毕后被调用,session关闭函数在关闭session时进行任何必要的清理操作,我们的数据库关闭处理函数只是简单关闭了数据库连接:
1
2
3
4
function close(){
mysql_close();
return true;
}
  • 当一个session完成后,销毁函数就会被调用,它负责清空打开函数被调用后创建的所有内容,在这个例子里,我们需要从数据表中删除session信息。
1
2
3
4
5
function destroy($session_id){
global $table;
mysql_query("DELETE FROM $table WHERE session_id = '$session_id'");
return true;
}
  • 最后一个处理函数,垃圾收集(garbage-collection)处理函数,它不时被调用来清除过期的session数据,这个函数会找到所有生命周期内未被使用的session数据,生命周期通过参数传入,数据库垃圾收集函数把数据表中所有最后修改时超过最大时间的记录删除:
1
2
3
4
5
function gc($max_time){
global $table;
mysql_query("DELETE FROM $table WHERE UNIX_TIMESTAMP(expiration) < UNIX_TIMESTAMP()-$max_time") or error_log("gc: " . mysql_error() . "\n",3,"/tmp/errors.log");
return true;
}
  • 在创建了所有处理函数之后,我们可以调用session_set_save_handler()函数来注册我们创建的函数,如本例中,调用函数:
1
session_set_save_handle('open','close','read','write','destroy','gc');
  • 你必须在调用session_strat()函数之前调用session_set_save_handler()函数,它通常由加入存储函数完成,并在每一个需要自定义session处理函数的页面文件中调用session_set_save_handler()函数,例如:
1
2
3
4
<?php
require_once 'database_store.inc';
session_start();
?>
  • 由于处理函数是在脚本输出后才调用的,所以所有产生输出的函数都不能被调用,如果发生了错误,应像我们之前那样用error_log()函数将错误记录到日志文件中。

Cookie和Session联用

  • 在你的自定义的session处理函数中同时使用cookie和session,你可以在多次访问间保持状态,所有当用户离开站点时应当忘记的状态,如用户在浏览哪个页面,可以用PHP内置的session来保持,而所有需要在用户多次访问间保持的状态,如唯一的用户ID,可以保存在cookie中,通过用户的ID,你可以从持久性的数据存储中(如数据库)得到用户更多的状态信息,如显示偏好,邮件地址等等。
  • 下例允许用户选择文字和背景的颜色并把这些值存入一个cookie中,任何在一周内对该页面的访问都将输出cookie中的color值。

示例7-15:在多次访问间维持状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
if($_POST['bgcolor']){
setcookie('bgcolor',$_POST['bgcolor'],time() + (60*60*24*7));
}
$bgcolor = empty($bgcolor) ? 'gray' : $bgcolor;
?>

<body bgcolor="<?= $bgcolor ?>">
<form action="<?= $_SERVER['PHP_SELF'] ?>" method="POST">
<select name="bgcolor">
<option value="gray">Gray</option>
<option value="white">White</option>
<option value="black">Black</option>
<option value="blue">Blue</option>
<option value="green">Green</option>
<option value="red">Red</option>
</select>
<input type="submit" />
</form>
</body>

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!