PHP - 真随机数和伪随机数

首先来了解一下真随机数和伪随机数的概念。

真随机数发生器
,英文为:true random number generators ,简称为:TRNGs,是利用不可预知的物理方式来产生的随机数。

伪随机数发生器
,英文为:pseudo-random number generators ,简称为:PRNGs,是计算机利用一定的算法来产生的。

对比一下两种办法产生的随机数的图片。

Random.org(利用大气噪音来生成随机数,而大气噪音是空气中的雷暴所产生的
)生成的随机位图:

真随机数发生器


Windows下PHP的rand()函数产生的随机图片:

伪随机数发生器

很显然,后者伪随机数发生器产生的图片有这明显的条纹。

利用PHP的rand随机函数产生这张图片的代码为:

//需要开启gd库
header("Content-type: image/png");
$im = imagecreatetruecolor(512, 512)
    or die("Cannot Initialize new GD image stream");
$white = imagecolorallocate($im, 255, 255, 255);
for ($y=0; $y<512; $y++) {
    for ($x=0; $x<512; $x++) {
        if (rand(0,1) === 1) {
            imagesetpixel($im, $x, $y, $white);
        }
    }
}
imagepng($im);
imagedestroy($im);

实际上也并不是所有的伪随机数发生器(PRNGs)效果都这么差的,只是恰好在Windows下的PHP的rand()函数是这样。如果是在linux下 测试相同的代码的话,所产生的图片也看不出明显的条纹。在Windows下如果用mt_rand()函数替代rand()函数的话效果也会好很多。这是由 于mt_rand()用了Mersenne Twister(马其塞旋转)算法来产生随机数。PHP的文档还说:mt_rand() 可以产生随机数值的平均速度比 libc 提供的 rand() 快四倍。

另外,Linux内核(1.3.30以上)包括了一个随机数发生器/dev/random
,对于很多安全目的是足够的。

下面是关于Linux的随机数发生器的原理介绍 :

Linux 操作系统提供本质上随机(或者至少具有强烈随机性的部件)的库数据。这些数据通常来自于设备驱动程序。例如,键盘驱动程序收集两个按键之间时间的信息,然后将这个环境噪声填入随机数发生器库。

随机数据存储在 熵池 (
linux内核维护了一个熵池用来收集来自设备驱动程序和其它来源的环境噪音。理论上,熵池中的数据是完全随机的,可以实现产生真随机数序列。为跟踪熵池中数据的随 机性,内核在将数据加入池的时候将估算数据的随机性,这个过程称作熵估算。熵估算值描述池中包含的随机数位数,其值越大表示池中数据的随机性越好。
)
中,它在每次有新数据进入时进行“搅拌”。这种搅拌实际上是一种数学转换,帮助提高随机性。当数据添加到熵池中 后,系统估计获得了多少真正随机位。

测定随机性的总量是很重要的。问题是某些量往往比起先考虑时看上去的随机性小。例如,添加表示自从上次按键盘以来秒数的 32 位数实际上并没有提供新的 32 位随机信息,因为大多数按键都是很接近的。

/dev/random
中读取字节后,熵池就使用 MD5 算法进行密码散列,该散列中的各个字节被转换成数字,然后返回。

如果在熵池中没有可用的随机性位, /dev/random
在池中有足够的随机性之前等待,不返回结果。这意味着如果使用 /dev/random
来产生许多随机数,就会发现它太慢了,不够实用。我们经常看到 /dev/random
生成几十字节的数据,然后在许多秒内都不产生结果。

幸运的是有熵池的另一个接口可以绕过这个限制:/dev/urandom。即使熵池中没有随机性可用,这个替代设备也总是返回随机数。如果您取出许 多数而不给熵池足够的时间重新充满,就再也不能获得各种来源的合用熵的好处了;但您仍可以从熵池的 MD5 散列中获得非常好的随机数!这种方式的问题是,如果有任何人破解了 MD5 算法,并通过查看输出了解到有关散列输入的信息,那么您的数就会立刻变得完全可预料。大多数专家都认为这种分析从计算角度来讲是不可行的。然而,仍然认为 /dev/urandom
/dev/random
要“不安全一些”(并通常值得怀疑)。

Windows下没有/dev/random可用,但可以使用微软的“capicom.dll”所提供的CAPICOM.Utilities 对象。

以下是使用PHP时比用mt_rand()函数产生更好的伪随机数的一段例子代码:

<?php
// get 128 pseudorandom bits in a string of 16 bytes
 
$pr_bits = '';
 
// Unix/Linux platform?
$fp = @fopen('/dev/urandom','rb');
if ($fp !== FALSE) {
    $pr_bits .= @fread($fp,16);
    @fclose($fp);
}
 
// MS-Windows platform?
if (@class_exists('COM')) {
    try {
        $CAPI_Util = new COM('CAPICOM.Utilities.1');
        $pr_bits .= $CAPI_Util->GetRandom(16,0);
 
        // if we ask for binary data PHP munges it, so we
        // request base64 return value.  We squeeze out the
        // redundancy and useless ==CRLF by hashing...
        if ($pr_bits) { $pr_bits = md5($pr_bits,TRUE); }
    } catch (Exception $ex) {
        // echo 'Exception: ' . $ex->getMessage();
    }
}
 
if (strlen($pr_bits) < 16) {
    // do something to warn system owner that
    // pseudorandom generator is missing
}
?>

所以PHP要产生真随机数
还是要调用外部元素来支持的.....

jquery url检测遇到的jquery跨域问题及JSONP的使用

jsonp的ibm上的资料:

使用 JSONP 实现跨域通信,第 1 部分: 结 合 JSONP 和 jQuery 快速构建强大的

 

跨域限制的原因

 跨域问题存在实际上源于浏览器的同源策略(same origin policy),简单讲,同源就是要求域名,协议,端口三者都一致;而同源策略就是指页面上的脚本不能访问非同源的资源(包括HTTP响应和Cookie);

    很多人会想到一个很熟悉的东西:document.domain

    同 源策略有点放松的就是:b.a.com上的页面无法通过a.com的同源验证,但是设置b.a.com页面的document.domain 属性为a.com,就可以通过浏览器对a.com的同源检测;但是,document.domain只允许设置成更上级的域名,而不是 其它域名,例如c.com就不行; 提到这里很多人都会想到多级域 名下共享Cookie的路子就是把Cooki设置成上级域名;在Web2.0的时代,这种本质上同域跨级解决方案远远不能满足我们跨域的需求;

 

JQUERY的解决办法

浏览器会进行同源检查,这导致了跨域 问题,然而这个跨域检查还有一个例外那就是HTML的<Script>标记;我们经常使用<Script>的src属性,脚本静 态资源放在独立域名下或者来自其它站点的时候这里是一个url;这个url响应的结果可以有很多种,比 如JSON,返回的Json值成为<Script>标签的src属性值.这种属性值变化并不会引起页面的影响.按照惯例,浏览器在URL的查询字符串中提 供一个参数,这个参数将作为结果的前缀一起返回到浏览器;

 

<script type="text/javascript" src="http://domain2.com/getjson?jsonp=parseResponse"> </script> 响应值:parseResponse({"Name": "Cheeso", "Rank": 7}) 注意这里的响应值parseResponse({"Name": "Cheeso", "Rank": 7}),JSONP中URL传入的参数实际上是个回调函数,网站返回的也不是纯JSON数据,如果只有数据,返回后浏览器不会有任何动作。

撇开JQuery,如果自己写JSONP调用,<script src="http://domain2.com/getjson?callback=load"></script>,这时候服返回的数据是类似 load({jsonp数据});(也就是说返回的那个调用函数随着你传入的 callback 而改变。) 这样的javascript代码,调用页面上的load函数,同时把数据传进去。

php json函数在jquery中的应用

 

本文来自小屋sjolzy.cn【原创】

 

鉴于JSON的娇小性和通用性 PHP在5.2的版本之后 默认加上一组针对数据进行JSON互转的函数: 

json_encode :数据到JSON 

json_decode :JSON到PHP变量的转换 

这两个函数在使用Ajax诸如:jquery和Ext框架的时候显得尤为重要,Ext默认接受的数据就是JSON格式,而jquery如果让JSON接入之后就显得更加轻松和简洁。本文将用一个实例讨论一下PHP 的JSON数据函数在jquery中的应用。 

Jquery做为一个轻量级的Ajax框架确实显得短小而精悍,比较适合Web编程的前台页面,更加适合和PHP结合。其中jquery中有一个方法是:$.getJSON,这个用来从服务器脚本页面取得JSON格式的数据的。我们按顺序来,首先建立一个名为json.php的PHP脚本,具体程序如下: 

<?php 

//创建一个联合数组,这里我们尽量真实的模拟了一个从数据库中读取的记录 

$a=array('name'=>'dog','sex'=>'f','par'=>array('li','wang','lian')); 

$b=array('name'=>'pig','sex'=>'m','par'=>array('liv','wange','liang')); 

$array = array(0=>$a,1=>$b); 

//将数组转化为一个json数据串 

echo json_encode($array); 

?>

 

前台页面jquery代码: 

<script> 

$(document).ready(function(){ 

    $.getJSON( 

    'json.php', 

    function(data){ 

       var content="";         

       for(var i=0;i<data.length;i++){//循环读取后台的取得的JSON数据 

        content+=data[i].name; 

        var par="排挡:"; 

        for(var j=0;j<data[i].par.length;j++){//循环读取数据中的par集合 

            par+=data[i].par[j]+"、"; 

        } 

        content+=par+"<br/>"; 

      } 

    alert(content); 

    } 

   );     

}); 

</script>

 

本例在循环读取数据的时候应用了js的基本流程控制for循环方法,在jquery中也提供了一个遍历方法each,底层实现也是基于for循环的。 

但按由于json_encode json_decode两个函数是在PHP5.2之后才有的,早于此前的版本使用将会报一个找不到json_encode函数的错误!

所以为了适用性,做了一下修改:

 

/*

 * 自定义my_json_encode函数

 * @params array $arr 欲转json的数组

 */

function my_json_encode($arr){

if (!function_exists('json_encode')){

return json_encode($arr);

}else{

require_once 'lib/json.class.php';

$json = new Services_JSON();

return $json->encode($arr);

}

}

自定义一个my_json_encode函数,当php版本为5.2之前,找不到son_encode函数时,载入json.class.php类文件。使用my_json_encode等价于json_encode。(json.class.php下载地址

PHP实现对文本数据库的数据显示、加入、修改、删除、查询

 PHP实现对文本数据库的数据显示、加入、修改、删除、查询五大基本操作的方法
我用一个留言本程序作为例子,阐述PHP实现对文本数据库的数据显示、加入、修改、删除、查询五大基本操作的方法。

此文本数据库共有字段10个:客户IP、发言时间、客户名、客户EMAIL、客户主页地址、留言表情图片名、客户QQ、客户形象图片、留言内容、管理员回复内容。

1、加入数据程序段。
$date=date("Y-m-d H:i:s");//取得系统时间
$ip = $HTTP_SERVER_VARS[REMOTE_ADDR]; //取得发言的IP地址
$text=encode($gb_text);//去掉留言内容后面的空格.
$fp=fopen("gb.dat","a");//以只写模式打开gb.dat文本文件,文件指针指向文件尾部.
$str =$ip."|".$date."|".$gb_name."|".$gb_email."|".$gb_home."|".$face."|".$gb_qq."|". $head."|".$text."|".$reply."\n";//将所有留言的数据赋予变量$str,"|"的目的是用来今后作数据分割时的数据间隔符号。
fwrite($fp,$str);//将数据写入文件
fclose($fp);//关闭文件


showmessage("留言成功!","index.php","3");//留言成功,3秒后自动返回主界面。
其中的$gb_name 、$gb_email、$gb_home、$face、$gb_qq、$head、$gb_text、$reply是由发言表单传过来的数据。
2、数据显示程序段
<?
if (file_exists("gb.dat")){//检测文件是否存在
$array=file("gb.dat");//将文件全部内容读入到数组$array
$arr=array_reverse($array);//将$array里的数据安行翻转排列(即最后一行当第一行,依此类推)读入数组$arr的每一个单元($arr[0]...)。
$num=count($array);//获取数组$array里的信息数(一行为一条信息)
if ($num>0){//如果信息数大于零(即文本数据库不为空)
$total=ceil($num/$pagesize);//计算总页数(取最大整数,即凡有小数点都进一取整,$pagesize为预设的每页显示的信息数)
if($page<1){//如果当前页面数码号小于1
$page=1;//则赋值为1
}
$number=($page-1)*$pagesize;//计算当前所显示第一个留言的数码号(数码号从零开始,主要是达到与数组单元号对应的目的)
for($i=0;$i<=$pagesize-1;$i++){//进入循环

$row=explode("|",$arr[$number]);//以"|"作为分割符,分割数组$arr中每第$number个单元的数据,并将这些数据赋予数组$rom
list($ip,$datetime,$name,$email,$home,$face,$qq,$head,$text,$reply)=$row;//将数组$row里的单元数据按顺序赋予括号里的变量
?
<img src=<? echo $head ?> >//显示客户形象图片
<br>
<font color="#0099CC">昵称【<? echo $name ?><font size="2">】<br>//显示客户名
发表于:<? echo $datetime ?>//显示留言发表时间
<br>
<img src=<? echo $face ?>>//显示客户留言表情图片
<? echo $name ?>说:<? echo $text; ?>//显示客户留言内容
<br>
<? echo $reply ?>//显示回复内容
<br>
<a href="<? _fcksavedurl=""<?" echo $home ?>" target="_blank">访问<? echo $name ?>的主页</a>//客户主页的超连接
<a href="mailto:<? echo $email ?>">给<? echo $name ?>发信</a>//客户E-MAIL的连接 网管u家www.bitscn.net
<? echo $name ?>的QQ号码是<? echo $qq ?>//显示客户的QQ号码
<? echo $name ?>的IP地址为<? echo $ip ?>" //显示客户的IP地址
<a href="reply.php?time=<? echo $datetime ?>">回复</a>//留言回复的连接语句
<a href="del.php?time=<? echo $datetime ?>">删除</a>//留言删除的语句(以客户留言时间$datetime作为删除标识)
<br>
<?
if ($number == $num-1)//如果数组的单元号等于总留言数减一(因为单元号以零开始的,所以这意味着这是最后一条留言)
{
break;//跳出循环
}
$number = $number + 1; //数组单元号加1
}//循环结束符
}
if ($page <> 1)//如果当前页面数码号不等于1
{
$back = $page - 1;//当前页面数码号减1,并将此值赋予变量$back
echo "<a href=index.php?page=1>第一页</a>";//显示第一页的连接
echo " <a href=index.php?page=$back>上一页</a>" ;当前页面数码号等于$back,并显示其连接

}
if ($page <> $total)//如果当前页面数码号不等于总页数号(最后一页数码号)
{
$next = $page + 1;//当前页面数码号加1并赋予变量$next
echo " <a href=index.php?page=$next>下一页</a>" ;//显示下一页连接
echo " <a href=index.php?page=$total>最后一页</a>"; 显示最后一页连接
}
echo "页数:$page / $total";//显示当前页面数码号和显示最后一页数码号
echo "共有 $num 条留言";//显示留言数信息
}
else {
echo "<center>当前没有任何留言!</center>";//如果文件内容为空则显示的信息
}
else {
echo "<center>数据文件丢失,请联系管理员!或发布留言重新建立数据文件!</center>";//如果文件不存在显示的信息
}
3、数据修改程序段
$list=file("gb.dat");//读取整个gb.dat文件到数组$list,数组每一个元素为一条留言($list[0]是第一条留言的数据、$list[1]是第一条留言的数据..... 网管有家www.bitscn.net
$n=count($list);//计算$list内容里的留言总数,并赋予变量$n
if ($n>0){ //如果留言数大于0
$fp=fopen("gb.dat","w");//则以只写模式打开文件gb.dat
$gb_reply=encode($gb_reply);
for ($i=0;$i<$n;$i++) {//进入循环
if(eregi($ttime,$list[$i])){//将送来发留言时间$ttime与数组单元$list里内容进行字串匹配比较
$f=explode("|",$list[$i]);//如果找到匹配,就以"|"作为分隔符,切开留言信息$list[$i](第$i条留言),并将这些数据赋予数组$f
$f[9]=$gb_reply;//将$f[9](留言信息最后一条数据)用$gb_reply(回复内容)代替。
$list[$i]=$f[0]."|".$f[1]."|".$f[2]."|".$f[3]."|".$f[4]."|".$f[5]."|".$f[6]."|".$f[7]."|".$f[8]."|".$f[9]."\n"; //将数组单元$list[$i]的内容用数组$f加上分隔符"|"代替(其中$f[9]是修改了的新数据)。
break;//跳出循环
}
}//循环结束符
}
FOR($i=0;$i<=$n;$i++){//进入循环
fwrite($fp,$list[$i]);//将数组$list的每个单元为一行,写入文件gb.dat 
}//循环结束符
fclose($fp);//关闭文件
showmessage("回复成功!","index.php");//回复成功,自动返回主界面。
4、数据删除程序段
$list=file("gb.dat");//读取整个gb.dat文件到数组$list,数组每一个元素为一条留言($list[0]是第一条留言的数据、$list[1]是第一条留言的数据.....
$n=count($list);//计算$list内容里的留言总数,并赋予变量$n
if ($n>0){//如果留言数大于0
$fp=fopen("gb.dat","w");//则以只写模式打开文件gb.dat
for ($i=0;$i<$n;$i++) {//进入循环
if(eregi($ttime,$list[$i])){//将发送过来发留言时间$ttime与数组$list[$i]里的字串进行匹配比较
$list[$i]="";//如果匹配成功,则将$list[$i]清空(达到删除的目的)
break;//跳出循环
}
}//循环结束符
FOR($i=0;$i<=$n;$i++){//进入循环
fwrite($fp,$list[$i]);//将数组$list的每个单元为一行,写入文件gb.dat
} //循环结束符
fclose($fp);//关闭文件
showmessage("删除成功!","index.php");//删除成功,自动返回主界面。

}

5、数据查询程序段
<form action="search.php" method="post">
<font color="#0099CC" size="2">搜索关键字:
<input name="found" type="text" id="found" style="background-color:#FFFFFF; color:#8888AA; border: 1 double #3399CC" size="12">
<input name="submit" type="image" src="image/search.gif" alt="留言搜索">
</font></td>
</tr>
</table>
</form>
////////////////////////////////上面是搜索表单语句段
<?
$id=0;
$list=file("gb.dat");//读取整个gb.dat文件到数组$list,数组每一个元素为一条留言($list[0]是第一条留言的数据、$list[1]是第一条留言的数据.....
$n=count($list);//计算$list内容里的留言总数,并赋予变量$n
$found=trim($found);
if (!$found){ //如果$found为假
echo "<center>您没有输入任何关键字!</center>";//作相关显示


}
else {
if($n>0){//如果留言数大于0
for ($i=0;$i<$n;$i++) {//进入循环
if(eregi($found,$list[$i])){//输入的关键字与数组$list[$i]里的字串进行匹配比较
$row=explode("|",$list[$i]); $id=1; //如果找到匹配,就以"|"作为分隔符,切开留言信息$list[$i](第$i条留言),并将这些数据赋予数组$row.并将变量$id赋予1,以便作为是否找到匹配的判断。
list($ip,$datetime,$name,$email,$home,$face,$qq,$head,$text,$reply)=$row;//将数组$row里的单元数据按顺序赋予括号里的变量
?>
<img src=<? echo $head ?> >//显示客户形象图片
<br>
<font color="#0099CC">昵称【<? echo $name ?><font size="2">】<br>//显示客户名
发表于:<? echo $datetime ?>//显示留言发表时间
<br>
<img src=<? echo $face ?>>//显示客户留言表情图片
<? echo $name ?>说:<? echo $text; ?>//显示客户留言内容
<br>
<? echo $reply ?>//显示回复内容
<a href="<? _fcksavedurl=""<?" _fcksavedurl=""<?" echo $home ?>" target="_blank">访问<? echo $name ?>的主页</a>//客户主页的超连接
<a href="mailto:<? echo $email ?>">给<? echo $name ?>发信</a>//客户E-MAIL的连接
<? echo $name ?>的QQ号码是<? echo $qq ?>//显示客户的QQ号码
<? echo $name ?>的IP地址为<? echo $ip ?>" //显示客户的IP地址
<a href="reply.php?time=<? echo $datetime ?>">回复</a>//留言回复的连接语句
<a href="del.php?time=<? echo $datetime ?>">删除</a>//留言删除的语句(以客户留言时间$datetime作为删除标识)
<br>
<?
}
}//循环结束符
}}
if($id==0){ echo "<center>没有找到与关键字匹配的留言!</center>";}//如果$id=0则表示没找到匹配,显示相关提示


?>

PHP 实现多服务器共享 SESSION 数据

 

PHP 实现多服务器共享 SESSION 数据

肖理达 (KrazyNio AT hotmail.com), 2005.09.13, 转载请注明出处



一、问题起源

稍大一些的网站,通常都会有好几个服务器,每个服务器运行着不同功能的模块,使用不同的二级域名,而一个整体性强的网站,用户系统是统一的,即一套用户名、密码在整个网站的各个模块中都是可以登录使用的。各个服务器共享用户数据是比较容易实现的,只需要在后端放个数据库服务器,各个服务器通过统一接口对用户数据进行访问即可。但还存在一个问题,就是用户在这个服务器登录之后,进入另一个服务器的别的模块时,仍然需要重新登录,这就是一次登录,全部通行的问题,映射到技术上,其实就是各个服务器之间如何实现共享 SESSION 数据的问题。

二、PHP SESSION 的工作原理

在解决问题之前,先来了解一下 PHP SESSION 的工作原理。在客户端(如浏览器)登录网站时,被访问的 PHP 页面可以使用 session_start() 打开 SESSION,这样就会产生客户端的唯一标识 SESSION ID(此 ID 可通过函数 session_id() 获取/设置)。SESSION ID 可以通过两种方式保留在客户端,使得请求不同的页面时,PHP 程序可以获知客户端的 SESSION ID;一种是将 SESSION ID 自动加入到 GET 的 URL 中,或者 POST 的表单中,默认情况下,变量名为 PHPSESSID;另一种是通过 COOKIE,将 SESSION ID 保存在 COOKIE 中,默认情况下,这个 COOKIE 的名字为 PHPSESSID。这里我们主要以 COOKIE 方式进行说明,因为应用比较广泛。 
那么 SESSION 的数据保存在哪里呢?当然是在服务器端,但不是保存在内存中,而是保存在文件或数据库中。默认情况下,php.ini 中设置的 SESSION 保存方式是 files(session.save_handler = files),即使用读写文件的方式保存 SESSION 数据,而 SESSION 文件保存的目录由 session.save_path 指定,文件名以 sess_ 为前缀,后跟 SESSION ID,如:sess_c72665af28a8b14c0fe11afe3b59b51b。文件中的数据即是序列化之后的 SESSION 数据了。如果访问量大,可能产生的 SESSION 文件会比较多,这时可以设置分级目录进行 SESSION 文件的保存,效率会提高很多,设置方法为:session.save_path="N;/save_path",N 为分级的级数,save_path 为开始目录。当写入 SESSION 数据的时候,PHP 会获取到客户端的 SESSION_ID,然后根据这个 SESSION ID 到指定的 SESSION 文件保存目录中找到相应的 SESSION 文件,不存在则创建之,最后将数据序列化之后写入文件。读取 SESSION 数据是也是类似的操作流程,对读出来的数据需要进行解序列化,生成相应的 SESSION 变量。

三、多服务器共享 SESSION 的主要障碍及解决办法

通过了解 SESSION 的工作原理,我们可以发现,在默认情况下,各个服务器会各自分别对同一个客户端产生 SESSION ID,如对于同一个用户浏览器,A 服务器产生的 SESSION ID 是 30de1e9de3192ba6ce2992d27a1b6a0a,而 B 服务器生成的则是 c72665af28a8b14c0fe11afe3b59b51b。另外,PHP 的 SESSION 数据都是分别保存在本服务器的文件系统中。如下图所示:

image 

确定了问题所在之后,就可以着手进行解决了。想要共享 SESSION 数据,那就必须实现两个目标:一个是各个服务器对同一个客户端产生的 SESSION ID 必须相同,并且可通过同一个 COOKIE 进行传递,也就是说各个服务器必须可以读取同一个名为 PHPSESSID 的 COOKIE;另一个是 SESSION 数据的存储方式/位置必须保证各个服务器都能够访问到。简单地说就是多服务器共享客户端的 SESSION ID,同时还必须共享服务器端的 SESSION 数据。

第一个目标的实现其实很简单,只需要对 COOKIE 的域(domain)进行特殊地设置即可,默认情况下,COOKIE 的域是当前服务器的域名/IP 地址,而域不同的话,各个服务器所设置的 COOKIE 是不能相互访问的,如 www.aaa.com 的服务器是不能读写 www.bbb.com 服务器设置的 COOKIE 的。这里我们所说的同一网站的服务器有其特殊性,那就是他们同属于同一个一级域,如:aaa.infor96.com 和 www.infor96.com 都属于域 .infor96.com,那么我们就可以设置 COOKIE 的域为 .infor96.com,这样 aaa.infor96.com、www.infor96.com 等等都可以访问此 COOKIE。PHP 代码中的设置方法如下:

ini_set('session.cookie_domain', '.infor96.com');

这样各个服务器共享同一客户端 SESSION ID 的目的就达到了。

第二个目标的实现可以使用文件共享方式,如 NFS 方式,但设置、操作上有些复杂。我们可以参考先前所说的统一用户系统的方式,即使用数据库来保存 SESSION 数据,这样各个服务器就可以方便地访问同一个数据源,获取相同的 SESSION 数据了。

解决办法如下图所示:

image 

四、代码实现

首先创建数据表,MySQL 的 SQL 语句如下:

   CREATE TABLE `sess` (
     `sesskey` varchar(32) NOT NULL default '',
      `expiry` bigint(20) NOT NULL default '0',
      `data` longtext NOT NULL,
      PRIMARY KEY  (`sesskey`),
      KEY `expiry` (`expiry`)
    ) TYPE=MyISAM

sesskey 为 SESSION ID,expiry 为 SESSION 过期时间,data 用于保存 SESSION 数据。

默认情况下 SESSION 数据是以文件方式保存,想要使用数据库方式保存,就必须重新定义 SESSION 各个操作的处理函数。PHP 提供了session_set_save_handle() 函数,可以用此函数自定义 SESSION 的处理过程,当然首先要先将 session.save_handler 改成 user,可在 PHP 中进行设置:

session_module_name('user');


接下来着重讲一下 session_set_save_handle() 函数,此函数有六个参数:

session_set_save_handler ( string open, string close, string read, string write, string destroy, string gc )

各个参数为各项操作的函数名,这些操作依次是:打开、关闭、读取、写入、销毁、垃圾回收。PHP 手册中有详细的例子,在这里我们使用 OO 的方式来实现这些操作,详细代码如下:

<?php define('MY_SESS_TIME', 3600);   //SESSION 生存时长 //类定义 class My_Sess {     function init()     {         $domain = '.infor96.com';         //不使用 GET/POST 变量方式         ini_set('session.use_trans_sid',    0);         //设置垃圾回收最大生存时间         ini_set('session.gc_maxlifetime',   MY_SESS_TIME);          //使用 COOKIE 保存 SESSION ID 的方式         ini_set('session.use_cookies',      1);         ini_set('session.cookie_path',      '/');         //多主机共享保存 SESSION ID 的 COOKIE         ini_set('session.cookie_domain',    $domain);          //将 session.save_handler 设置为 user,而不是默认的 files         session_module_name('user');         //定义 SESSION 各项操作所对应的方法名:         session_set_save_handler(             array('My_Sess', 'open'),   //对应于静态方法 My_Sess::open(),下同。             array('My_Sess', 'close'),             array('My_Sess', 'read'),             array('My_Sess', 'write'),             array('My_Sess', 'destroy'),             array('My_Sess', 'gc')         );     }   //end function      function open($save_path, $session_name) {         return true;     }   //end function      function close() {         global $MY_SESS_CONN;          if ($MY_SESS_CONN) {    //关闭数据库连接             $MY_SESS_CONN->Close();         }         return true;     }   //end function      function read($sesskey) {         global $MY_SESS_CONN;          $sql = 'SELECT data FROM sess WHERE sesskey=' . $MY_SESS_CONN->qstr($sesskey) . ' AND expiry>=' . time();         $rs =& $MY_SESS_CONN->Execute($sql);         if ($rs) {             if ($rs->EOF) {                 return '';             } else {    //读取到对应于 SESSION ID 的 SESSION 数据                 $v = $rs->fields[0];                 $rs->Close();                 return $v;             }   //end if         }   //end if         return '';     }   //end function      function write($sesskey, $data) {         global $MY_SESS_CONN;                  $qkey = $MY_SESS_CONN->qstr($sesskey);         $expiry = time() + My_SESS_TIME;    //设置过期时间                  //写入 SESSION         $arr = array(             'sesskey' => $qkey,             'expiry'  => $expiry,             'data'    => $data);         $MY_SESS_CONN->Replace('sess', $arr, 'sesskey', $autoQuote = true);         return true;     }   //end function      function destroy($sesskey) {         global $MY_SESS_CONN;          $sql = 'DELETE FROM sess WHERE sesskey=' . $MY_SESS_CONN->qstr($sesskey);         $rs =& $MY_SESS_CONN->Execute($sql);         return true;     }   //end function      function gc($maxlifetime = null) {         global $MY_SESS_CONN;          $sql = 'DELETE FROM sess WHERE expiry<' . time();         $MY_SESS_CONN->Execute($sql);         //由于经常性的对表 sess 做删除操作,容易产生碎片,         //所以在垃圾回收中对该表进行优化操作。         $sql = 'OPTIMIZE TABLE sess';         $MY_SESS_CONN->Execute($sql);         return true;     }   //end function }   ///:~  //使用 ADOdb 作为数据库抽象层。 require_once('adodb/adodb.inc.php'); //数据库配置项,可放入配置文件中(如:config.inc.php)。 $db_type = 'mysql'; $db_host = '192.168.212.1'; $db_user = 'sess_user'; $db_pass = 'sess_pass'; $db_name = 'sess_db'; //创建数据库连接,这是一个全局变量。 $GLOBALS['MY_SESS_CONN'] =& ADONewConnection($db_type); $GLOBALS['MY_SESS_CONN']->Connect( $db_host, $db_user, $db_pass, $db_name); //初始化 SESSION 设置,必须在 session_start() 之前运行!! My_Sess::init(); ?>


五、遗留问题

如果网站的访问量很大的话,SESSION 的读写会频繁地对数据库进行操作,这样效率就会明显降低。考虑到 SESSION 数据一般不会很大,可以尝试用 C/Java 写个多线程的程序,用 HASH 表保存 SESSION 数据,并通过 socket 通信进行数据读写,这样 SESSION 就保存在内存中,读写速度应该会快很多。另外还可以通过负载均衡来分担服务器负载。不过这些都只是我自己的一些想法和假设,并没有实践过 :( 。。。。。。