discuzx2系列之session的实现机制

discuzx2系列文章

discuzx2系列之群组积分排行

我们知道discuzx是没有采用php默认的session机制的,而是自己采用数据库表自己进行了构建一套session用户状态识别机制;而不是简单的把php session采用存入数据库的方式。

直接看代码

几个顶级的control入口是引用了下面的class_core.php;也就是discuzx核心类文件。


require './source/class/class_core.php';
$discuz = & discuz_core::instance();

$discuz->init();

可以再discuz_core类里面的,初始化了一系列模块


function init() {
if(!$this->initated) {
$this->_init_db();
$this->_init_memory();
$this->_init_user();
$this->_init_session();    // 初始化session
$this->_init_setting();
$this->_init_mobile();
$this->_init_cron();
$this->_init_misc();
$this->_init_connect();
}
$this->initated = true;
}

那就来看看_init_session()  这个函数的主要作用初始化session,检测已有session或者创建新的session作为当前连接的唯一标识
其中附加功能包括,当前连接用户ip的白名单,黑名单检测;确定用户的详细属性信息,如是新登陆的用户则更新用户的状态信息和在线统计信息


function _init_session() {
$this->session = new discuz_session();  // 实例化数据库session类

if($this->init_session)    {
// $this->var 就是超全局变量$_G 可以看到$this->var = & $_G;是在 _init_env()中引用赋值的
// 将目前cookie为sid的值和用户个人信息传入init;discuz_session 的init方法始终会返回一个实例对象,来表示当前的用户或游客
$this->session->init($this->var['cookie']['sid'], $this->var['clientip'], $this->var['uid']);

$this->var['sid'] = $this->session->sid;
$this->var['session'] = $this->session->var; // 当前用户对象的session数据

// 如果当前返回sid 不等于浏览器中名为sig cookie的值,则设置新的sid值1天后过期
if($this->var['sid'] != $this->var['cookie']['sid']) {
dsetcookie('sid', $this->var['sid'], 86400);
}
// $isnew 表示是否为网站新的用户,也就是新创建的session
if($this->session->isnew) {
// 是新来访用户,检测用户的ip是否在系统的可访问名单($_G['setting']['ipaccess'])或是否在系统的黑名单(common_banned禁止访问表;cache_ipbanned.php)中
if(ipbanned($this->var['clientip'])) {
// ip不在可访问名单或者ip在系统的黑名单中;则将用户移入 groupid=6的“禁止ip”组;游客的groupid是7
$this->session->set('groupid', 6);
}
}

// 如果是“禁止ip”组,显示系统提示信息
// "抱歉,您的 IP 地址不在被允许,或您的账号被禁用,无法访问本站点"
if($this->session->get('groupid') == 6) {
$this->var['member']['groupid'] = 6;
sysmessage('user_banned');
}
// 存在当前登录用户;用户是新登录的或者lastactivity最后活动时间在十分钟以前了
if($this->var['uid'] && ($this->session->isnew || ($this->session->get('lastactivity') + 600) < TIMESTAMP)) {
// 重新设置lastactivity,接下来在footer.html中调用updatesession();时更新当前用户session记录信息 主要作用就是更新lastactivity;避免按在线时间$_G['setting']['onlinehold']过期被清除掉
$this->session->set('lastactivity', TIMESTAMP);
if($this->session->isnew) {
// 新的登录用户,更新用户的状态
DB::update('common_member_status', array('lastip' => $this->var['clientip'], 'lastvisit' => TIMESTAMP), "uid='".$this->var['uid']."'");
}
}

}
}

discuz_session类;主要作用对session进行操作,对应的是common_session记录表。其中$discuz->session 返回的就是浏览器当前连接用户的session实例对象了。($discuz就是discuz_core的引用实例)


class discuz_session {

var $sid = null;
var $var;
var $isnew = false;
var $newguest = array('sid' => 0, 'ip1' => 0, 'ip2' => 0, 'ip3' => 0, 'ip4' => 0,
'uid' => 0, 'username' => '', 'groupid' => 7, 'invisible' => 0, 'action' => 0,
'lastactivity' => 0, 'fid' => 0, 'tid' => 0, 'lastolupdate' => 0, 'country' => 0, 'email' => ''); //add country ,email

var $old =  array('sid' =>  '', 'ip' =>  '', 'uid' =>  0);

// 实例化,初始化session默认数据
function discuz_session($sid = '', $ip = '', $uid = 0) {
$this->old = array('sid' =>  $sid, 'ip' =>  $ip, 'uid' =>  $uid);
$this->var = $this->newguest;
if(!empty($ip)) {
$this->init($sid, $ip, $uid);
}
}

function set($key, $value) {
if(isset($this->newguest[$key])) {
$this->var[$key] = $value;
} elseif ($key == 'ip') {
$ips = explode('.', $value);
$this->set('ip1', $ips[0]);
$this->set('ip2', $ips[1]);
$this->set('ip3', $ips[2]);
$this->set('ip4', $ips[3]);
}
}

function get($key) {
if(isset($this->newguest[$key])) {
return $this->var[$key];
} elseif ($key == 'ip') {
return $this->get('ip1').'.'.$this->get('ip2').'.'.$this->get('ip3').'.'.$this->get('ip4');
}
}
// 初始化返回session实例对象
function init($sid, $ip, $uid) {
$this->old = array('sid' =>  $sid, 'ip' =>  $ip, 'uid' =>  $uid);
$session = array();
// cookie sid存在;读取记录
if($sid) {
$session = DB::fetch_first("SELECT * FROM ".DB::table('common_session').
" WHERE sid='$sid' AND CONCAT_WS('.', ip1,ip2,ip3,ip4)='$ip'");
}
// 不存在或存在session记录但与新的uid值不匹配
if(empty($session) || $session['uid'] != $uid) {
// 重新创建session记录
$session = $this->create($ip, $uid);
}

$this->var = $session;
$this->sid = $session['sid'];
}

// 返回新设置的session对象
function create($ip, $uid) {

$this->isnew = true;
$this->var = $this->newguest;
$this->set('sid', random(6));
$this->set('uid', $uid);
$this->set('ip', $ip);

//写入区域信息
include_once libfile('function/misc');
$ipaddress = convertip($ip);
//$ipaddress = mb_convert_encoding($ipaddress, 'UTF-8', 'GB2312');
$country = mb_substr($ipaddress, 2, 2, 'UTF-8');  // 获取两个字存入数据库

include_once libfile('local', 'include');
if (array_key_exists($country, $local_name)) { //如果数据在数组中
$country = $local_name[$country];
} else {// 如果不是则随机获取一个
$country = $local_name[array_rand($local_name)];
}
$this->set('country', $country);

$uid && $this->set('invisible', getuserprofile('invisible'));
$this->set('lastactivity', time());
$this->sid = $this->var['sid'];

return $this->var;
}

// 清除session记录,也就是清除掉已过期的连接
function delete() {

global $_G;
// onlinehold 系统设置的用户在线时间(分钟) 15
// 这里的$_G['setting']['onlinehold']已经换算成秒了;build_cache_setting()
$onlinehold = $_G['setting']['onlinehold'];
// 游客活动时间 1分钟
$guestspan = 60;
$onlinehold = time() - $onlinehold;
$guestspan = time() - $guestspan;

$condition = " sid='{$this->sid}' ";
$condition .= " OR lastactivity<$onlinehold ";  // 活动时间距离现在超过了15分钟了
$condition .= " OR (uid='0' AND ip1='{$this->var['ip1']}' AND ip2='{$this->var['ip2']}' AND ip3='{$this->var['ip3']}' AND ip4='{$this->var['ip4']}' AND lastactivity>$guestspan) "; // 同一ip,访问活动时间在1分钟以内
$condition .= $this->var['uid'] ? " OR (uid='{$this->var['uid']}') " : ''; // 存在uid 也可按uid删除
DB::delete('common_session', $condition);
}

// 更新记录
function update() {
global $_G;
if($this->sid !== null) {

$data = daddslashes($this->var);
// 新创建用户session记录;且对符合提交的session记录进行清除
if($this->isnew) {
$this->delete();
DB::insert('common_session', $data, false, false, true);
} else {
// 更新session记录
DB::update('common_session', $data, "sid='$data[sid]'");
}
$_G['session'] = $data;
// 更新sid过期时间,等于是每次刷新都会重新更新
dsetcookie('sid', $this->sid, 86400);
}
}

// 在线人数统计 登录用户/游客统计
function onlinecount($type = 0) {
$condition = $type == 1 ? ' WHERE uid>0 ' : ($type == 2 ? ' WHERE invisible=1 ' : '');
return DB::result_first("SELECT count(*) FROM ".DB::table('common_session').$condition);
}

}

对应的php session中的有效期,回收机制就是delete()方法;写入与读取对应的就是update();那用户在浏览器端刷新后是怎么进行session的验证与返回的呢?session开启后,用户每次请求服务器都会进行数据验证与返回的;所以discuzx每次请求都会执行这个update()

discuzx采用的是页面载入模板的形式,对应的 template/default/common/footer.htm是每个页面都可能会加载的;有一些公共内容在这里,其中就有用户的session更新函数调用

<!–{eval updatesession();}–>

updatesession()函数


function updatesession($force = false) {

global $_G;
static $updated = false;

if(!$updated) {
// 登录用户
if($_G['uid']) {
// 获取当前用户最后活动时间且authcode编码后写入到浏览器cookie中
if($_G['cookie']['ulastactivity']) {
$ulastactivity = authcode($_G['cookie']['ulastactivity'], 'DECODE');
} else {
$ulastactivity = getuserprofile('lastactivity');
// 有效期长达一年
dsetcookie('ulastactivity', authcode($ulastactivity, 'ENCODE'), 31536000);
}
}
// 实例化核心类
$discuz = & discuz_core::instance();
// $discuz->session 当前这个用户的session对象

// 用户在线时间更新时长(分钟) 也就是在线用户多久更新一次自己的“在线时间”
// Discuz! 可统计每个用户总共和当月的在线时间,本设置用以设定更新用户在线时间的时间频率。例如设置为 10,则用户每在线 10 分钟更新一次记录。本设置值越小,则统计越精确,但消耗资源越大。建议设置为 5~30 范围内,0 为不记录用户在线时间
$oltimespan = $_G['setting']['oltimespan'];
// session记录中的“用户在线时间更新”的最后时间
$lastolupdate = $discuz->session->var['lastolupdate'];

// 登录用户 且存在“在线时间更新”频率 且时间段已过则进行用户的个人在线时间更新。
// 等于是用户在线时间是按每次10分钟,这样叠加的;间隔越小越准确。
if($_G['uid'] && $oltimespan && TIMESTAMP - ($lastolupdate ? $lastolupdate : $ulastactivity) > $oltimespan * 60)      {
DB::query("UPDATE ".DB::table('common_onlinetime')."
SET total=total+'$oltimespan', thismonth=thismonth+'$oltimespan', lastupdate='" . TIMESTAMP . "'
WHERE uid='{$_G['uid']}'");
if(!DB::affected_rows()) {
DB::insert('common_onlinetime', array(
'uid' => $_G['uid'],
'thismonth' => $oltimespan,
'total' => $oltimespan,
'lastupdate' => TIMESTAMP,
));
}
// 重新设置 “用户在线时间更新”的最后更新时间
$discuz->session->set('lastolupdate', TIMESTAMP);
}
foreach($discuz->session->var as $k => $v) {
if(isset($_G['member'][$k]) && $k != 'lastactivity') {
$discuz->session->set($k, $_G['member'][$k]);
}
}

foreach($_G['action'] as $k => $v) {
$discuz->session->set($k, $v);
}

// 这里进行session状态更新
$discuz->session->update();

$updated = true;
// 更新当前登录用户的统计信息与用户状态信息
if($_G['uid'] && TIMESTAMP - $ulastactivity > 21600) {
if($oltimespan && TIMESTAMP - $ulastactivity > 43200) {
$total = DB::result_first("SELECT total FROM ".DB::table('common_onlinetime')." WHERE uid='$_G[uid]'");
// 换算为分钟写入用户个人统计信息表
DB::update('common_member_count', array('oltime' => round(intval($total) / 60)), "uid='$_G[uid]'", 1);
}
dsetcookie('ulastactivity', authcode(TIMESTAMP, 'ENCODE'), 31536000);
DB::update('common_member_status', array('lastip' => $_G['clientip'], 'lastactivity' => TIMESTAMP, 'lastvisit' => TIMESTAMP), "uid='$_G[uid]'", 1);
}
}
return $updated;
}

session在程序中的流程。用户访问页面,初始化discuz_session实例;读取客户端cookie sid 查找对应的session;已存在则返回记录信息,设置cookie sid的过期时间;不存在则创建新的session记录,也就是一个新的用户,在创建新用户前,对session记录表按系统设置的用户在线时间、sid、uid、同一ip1分钟内活动记录这几个条件进行清理;(很关键的一点是只有在创建新用户session记录时,才会进行session记录的删除整理) 登陆用户与session的关系这里是一个很简单的实现,也就是

_init_session(); // 也就是$this->var[‘uid’] 是否是有值得,也就是$_G[‘uid’]是否存在。uid等用户个人信息赋值的过程是在login成功后调用setloginstatus($member,$lifetime);函数(function_member.php)

$this->session->init($this->var[‘cookie’][‘sid’], $this->var[‘clientip’], $this->var[‘uid’]);

这样用户uid就直接与session记录关联起来了。同时登陆用户会进行“个人在线时间信息”“用户个人状态”的数据更新处理。

common_session表结构

CREATE TABLE pre_common_session (
sid char(6) NOT NULL default ” COMMENT ‘sid’,
ip1 tinyint(3) unsigned NOT NULL default ‘0’ COMMENT ‘IP段’,
ip2 tinyint(3) unsigned NOT NULL default ‘0’ COMMENT ‘IP段’,
ip3 tinyint(3) unsigned NOT NULL default ‘0’ COMMENT ‘IP段’,
ip4 tinyint(3) unsigned NOT NULL default ‘0’ COMMENT ‘IP段’,
uid mediumint(8) unsigned NOT NULL default ‘0’ COMMENT ‘会员id’,
username char(15) NOT NULL default ” COMMENT ‘会员名’,
groupid smallint(6) unsigned NOT NULL default ‘0’ COMMENT ‘会员组’,
invisible tinyint(1) NOT NULL default ‘0’ COMMENT ‘是否隐身登录’,
`action` tinyint(1) unsigned NOT NULL default ‘0’ COMMENT ‘当前动作’,
lastactivity int(10) unsigned NOT NULL default ‘0’ COMMENT ‘最后活动时间’,
lastolupdate int(10) unsigned NOT NULL default ‘0’ COMMENT ‘在线时间最后更新’,
fid mediumint(8) unsigned NOT NULL default ‘0’ COMMENT ‘论坛id’,
tid mediumint(8) unsigned NOT NULL default ‘0’ COMMENT ‘主题id’,
UNIQUE KEY sid (sid),
KEY uid (uid)
) ENGINE=MEMORY DEFAULT CHARSET=utf8 COMMENT=’会员认证表’;

需要注意的是session表采用的是memory引擎,优点是常驻内存存取速度快,方便操作与管理;缺点则是如果服务器重启则数据都被清空,只读或读为主的访问模式(短时间用户激增写入量会很大)

更多了解:

MySQL Memory存储引擎:优势及性能测试

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*