声明:本文内容纯属个人观点,官方保留最终解释
在上文(Discuz!NT URL地址重写) 中, 聊到了“在线用户”功能,因为当时介绍的重点不是“在线”
那一块,所以没做深入介绍。这就为今天这篇文章埋下了“伏笔”。因为在线这个功能太重要了,大家不妨
用VS打开我们产品的最新源码,然后搜索一下“OnlineUsers.”这个内容就会看到它在产品中被使用的“频
率”。
好了,言归正传,下面就开始接着上一篇文章中所说的“OnlineUsers.ResetOnlineList();” 方法介绍
一下用户在线功能。
首先请大家打开Discuz.Forum这个项目,从中找到"OnlineUsers.cs"这个文件,打开它找到
"ResetOnlineList()"这个函数,它的代码如下:
2 /// 复位在线表, 如果系统未重启, 仅是应用程序重新启动, 则不会重新创建
3 /// </summary>
4 /// <returns></returns>
5 public static int ResetOnlineList()
6 {
7 try
8 {
9 // 取得在线表最后一条记录的tickcount字段 (因为本功能不要求特别精确)
10 //int tickcount = DatabaseProvider.GetInstance().GetLastTickCount();
11 // 如果距离现在系统运行时间小于10分钟
12 if (System.Environment.TickCount < 600000)
13 {
14 return InitOnlineList();
15 }
16 return -1;
17 }
18 catch
19 {
20 try
21 {
22 return InitOnlineList();
23 }
24 catch
25 {
26 return -1;
27 }
28 }
29
30 }
31
32
这个函数本身就是在系统启动之后的10分钟内运行InitOnlineList函数,而为什么是10分钟(
不是别的时间段呢?),主要是因为这个值是个“估计值”,因为在线功能是系统的核心功能之一,
换句话说,系统在启动10分钟内,只要是用户在前台进行操作,绝对会用到这个核心功能,而这个
功能本身是依赖于数据库中的“dnt_onlines"表的(如果大家想了解这个表的结构,可以下载我们
产品的“数据字典”,里面有这方面内容的介绍)。所以创建(复位)和初始化这个数据表的责任就
交给了ResetOnlineList()这个函数(因为它是在HttpModule.cs中被绑定的,可以看作是系统运行
的起点,详情见上文)。
好了,即然清楚了这个函数的作用,不妨再了解一下代码中InitOnlineList()方法,它的作用
就是运行下面这个SQL语句:
2 public int CreateOnlineTable()
3 {
4 try
5 {
6 StringBuilder sb = new StringBuilder();
7 sb.Append("IF EXISTS (SELECT * FROM SYSOBJECTS WHERE id = object_id(N'[dnt_online]') AND OBJECTPROPERTY(id, N'IsUserTable') = 1) DROP TABLE [dnt_online];");
8 sb.Append("CREATE TABLE [dnt_online] ([olid] [int] IDENTITY (1, 1) NOT NULL,[userid] [int] NOT NULL,[ip] [varchar] (15) NOT NULL,[username] [nvarchar] (20) NOT NULL,[nickname] [nvarchar] (20) NOT NULL,[password] [char] (32) NOT NULL,[groupid] [smallint] NOT NULL,[olimg] [varchar] (80) NOT NULL,[adminid] [smallint] NOT NULL,[invisible] [smallint] NOT NULL,[action] [smallint] NOT NULL,[lastactivity] [smallint] NOT NULL,[lastposttime] [datetime] NOT NULL,[lastpostpmtime] [datetime] NOT NULL,[lastsearchtime] [datetime] NOT NULL,[lastupdatetime] [datetime] NOT NULL,[forumid] [int] NOT NULL,[forumname] [nvarchar] (50) NOT NULL,[titleid] [int] NOT NULL,[title] [nvarchar] (80) NOT NULL,[verifycode] [varchar] (10) NOT NULL ) ON [PRIMARY];");
9 sb.Append("ALTER TABLE [dnt_online] WITH NOCHECK ADD CONSTRAINT [PK_dnt_online] PRIMARY KEY CLUSTERED ([olid]) ON [PRIMARY]; ");
10 sb.Append("ALTER TABLE [dnt_online] ADD CONSTRAINT [DF_dnt_online_userid] DEFAULT ((-1)) FOR [userid],CONSTRAINT [DF_dnt_online_ip] DEFAULT ('0.0.0.0') FOR [ip],CONSTRAINT [DF_dnt_online_username] DEFAULT ('') FOR [username],CONSTRAINT [DF_dnt_online_nickname] DEFAULT ('') FOR [nickname],CONSTRAINT [DF_dnt_online_password] DEFAULT ('') FOR [password],CONSTRAINT [DF_dnt_online_groupid] DEFAULT (0) FOR [groupid],CONSTRAINT [DF_dnt_online_olimg] DEFAULT ('') FOR [olimg],CONSTRAINT [DF_dnt_online_adminid] DEFAULT (0) FOR [adminid],CONSTRAINT [DF_dnt_online_invisible] DEFAULT (0) FOR [invisible],CONSTRAINT [DF_dnt_online_action] DEFAULT (0) FOR [action],CONSTRAINT [DF_dnt_online_lastactivity] DEFAULT (0) FOR [lastactivity],CONSTRAINT [DF_dnt_online_lastposttime] DEFAULT ('1900-1-1 00:00:00') FOR [lastposttime],CONSTRAINT [DF_dnt_online_lastpostpmtime] DEFAULT ('1900-1-1 00:00:00') FOR [lastpostpmtime],CONSTRAINT [DF_dnt_online_lastsearchtime] DEFAULT ('1900-1-1 00:00:00') FOR [lastsearchtime],CONST RAINT [DF_dnt_online_lastupdatetime] DEFAULT (getdate()) FOR [lastupdatetime],CONSTRAINT [DF_dnt_online_forumid] DEFAULT (0) FOR [forumid],CONSTRAINT [DF_dnt_online_forumname] DEFAULT ('') FOR [forumname],CONSTRAINT [DF_dnt_online_titleid] DEFAULT (0) FOR [titleid],CONSTRAINT [DF_dnt_online_title] DEFAULT ('') FOR [title],CONSTRAINT [DF_dnt_online_verifycode] DEFAULT ('') FOR [verifycode];");
11 sb.Append("CREATE INDEX [forum] ON [dnt_online]([userid], [forumid], [invisible]) ON [PRIMARY];");
12 sb.Append("CREATE INDEX [invisible] ON [dnt_online]([userid], [invisible]) ON [PRIMARY];");
13 sb.Append("CREATE INDEX [forumid] ON [dnt_online]([forumid]) ON [PRIMARY];");
14 sb.Append("CREATE INDEX [password] ON [dnt_online]([userid], [password]) ON [PRIMARY];");
15 sb.Append("CREATE INDEX [ip] ON [dnt_online]([userid], [ip]) ON [PRIMARY];");
16
17 return DbHelper.ExecuteNonQuery(CommandType.Text, sb.Replace("dnt_", BaseConfigs.GetBaseConfig().Tableprefix).ToString());
18 }
19 catch
20 {
21 return -1;
22 }
23 }
24
25
该方法如果正常运行的话,会在数据库中建立dnt_onlines"这个表。用于记录在线用户的全部
信息。
上文介绍的仅仅是系统“初始化”时所要做的“活”。而更重要的内容是用户从一访问论坛并
执行一系列操作(如登陆,发贴,浏览版块等)时,“在线用户”机制在里面所启的重要作用。
这里先以登陆(系统)这一操作发生时,程序所做出的响应为例,看一下在线这块是如何进行绑
定的,请看如下代码(位于discuz.web项目的“aspx/1/”文件夹下的login.aspx.cs文件):
2 OnlineUsers.UpdateAction(olid, UserAction.Login.ActionID, 0, config.Onlinetimeout);
3
4
它的作用就是将已成功登陆系统的用户所执行的当前动作及相关信息更新到"dnt_onlines" 表
中。而这个函数本身所用到的四个参数要重点介绍一下:
olid: 在线列表id,对应"dnt_onlines"表中的olid字段
action:动作(结构类型,详情参见discuz.forum项目下的forumutils.cs文件)
用于传递动作的相关信息如:ActionName动作名称和ActionDescription动作描述等)
inid: 所在位置代码,即当前用户所访问的主题(TopicId)。因为登陆不牵扯主题操作,所
以上面的值为0
timeout: 无动作离线时间(config.Onlinetimeout),这个数据是在后台进行设置的,见下图:
timeout的作用就是当有别人访问论坛时,在更新自身在线状态信息同时,用这个设置数据(
整型)与在线表(dnt_onlines)中的"lastupdatetime"字段(最后活动时间)进行比较,找出超过
timeout规定的时间的用户,将其在线状态(onlinestate)设置为0(即为离线)。而做这件事的函
数就是在项目“discuz.data.sqlserver”下的“UserManage.cs”中的“AddOnlineUser”函数,这
里将它的代码贴出来,详情见注释:
2 /// 执行在线用户向表及缓存中添加的操作。
3 /// </summary>
4 /// <param name="__onlineuserinfo">在组用户信息内容</param>
5 /// <returns>添加成功则返回刚刚添加的olid,失败则返回0</returns>
6 public int AddOnlineUser(OnlineUserInfo __onlineuserinfo, int timeout)
7 {
8
9 string strDelTimeOutSql = "";
10 // 此处的设置见后台forum_uisetting.aspx.cs源码
11 // 如果timeout为负数则代表不需要精确更新用户是否在线的状态
12 if (timeout > 0)
13 {
14 if (__onlineuserinfo.Userid > 0)
15 {
16 strDelTimeOutSql = string.Format("{0}UPDATE [{1}users] SET [onlinestate]=1 WHERE [uid]={2};", strDelTimeOutSql, BaseConfigs.GetTablePrefix, __onlineuserinfo.Userid.ToString());
17 }
18 }
19 else
20 {
21 timeout = timeout * -1;
22 }
23
24 if (timeout > 9999)
25 {
26 timeout = 9999;
27 }
28
29 System.Text.StringBuilder sb = new System.Text.StringBuilder();
30 System.Text.StringBuilder sb2 = new System.Text.StringBuilder();
31
32 IDataReader dr = DbHelper.ExecuteReader(CommandType.Text, string.Format("SELECT [userid] FROM [{0}online] WHERE [lastupdatetime]<'{1}'", BaseConfigs.GetTablePrefix, DateTime.Parse(DateTime.Now.AddMinutes(timeout * -1).ToString("yyyy-MM-dd HH:mm:ss"))));
33 try
34 {
35 while (dr.Read())
36 {
37 sb.Append(",");
38 sb.Append(dr[0].ToString());
39 if (dr[0].ToString() != "-1")
40 {
41 sb2.Append(",");
42 sb2.Append(dr[0].ToString());
43 }
44 }
45 }
46 finally
47 {
48 dr.Close();
49 }
50
51 if (sb.Length > 0)
52 {
53 sb.Remove(0, 1);
54 strDelTimeOutSql = string.Format("{0}DELETE FROM [{1}online] WHERE [userid] IN ({2});", strDelTimeOutSql, BaseConfigs.GetTablePrefix, sb.ToString());
55 }
56 if (sb2.Length > 0)
57 {
58 sb2.Remove(0, 1);
59 strDelTimeOutSql = string.Format("{0}UPDATE [{1}users] SET [onlinestate]=0,[lastactivity]=GETDATE() WHERE [uid] IN ({2});", strDelTimeOutSql, BaseConfigs.GetTablePrefix, sb2.ToString());
60 }
61
62
63 DbParameter[] prams = {};
64 int olid = Utils.StrToInt(DbHelper.ExecuteScalar(CommandType.Text, strDelTimeOutSql + "INSERT INTO [" + BaseConfigs.GetTablePrefix + "online] ([userid],[ip],[username],[nickname],[password],[groupid],[olimg],[adminid],[invisible],[action],[lastactivity],[lastposttime],[lastpostpmtime],[lastsearchtime],[lastupdatetime],[forumid],[forumname],[titleid],[title], [verifycode])VALUES(@userid,@ip,@username,@nickname,@password,@groupid,@olimg,@adminid,@invisible,@action,@lastactivity,@lastposttime,@lastpostpmtime,@lastsearchtime,@lastupdatetime,@forumid,@forumname,@titleid,@title,@verifycode);SELECT SCOPE_IDENTITY()", prams).ToString(), 0);
65
66 // 如果id值太大则重建在线表
67 if (olid > 2147483000)
68 {
69 CreateOnlineTable();
70 DbHelper.ExecuteNonQuery(CommandType.Text, strDelTimeOutSql + "INSERT INTO [" + BaseConfigs.GetTablePrefix + "online] ([userid],[ip],[username],[nickname],[password],[groupid],[olimg],[adminid],[invisible],[action],[lastactivity],[lastposttime],[lastpostpmtime],[lastsearchtime],[lastupdatetime],[forumid],[titleid],[verifycode])VALUES(@userid,@ip,@username,@nickname,@password,@groupid,@olimg,@adminid,@invisible,@action,@lastactivity,@lastposttime,@lastpostpmtime,@lastsearchtime,@lastupdatetime,@forumid,@forumname,@titleid,@title,@verifycode);SELECT SCOPE_IDENTITY()", prams);
71 return 1;
72 }
73
74
75 return 0;
76 //return (int)DbHelper.ExecuteDataset(CommandType.Text, "SELECT [olid] FROM ["+BaseConfigFactory.GetTablePrefix+"online] WHERE [userid]=" + __onlineuserinfo.Userid.ToString()).Tables[0].Rows[0][0];
77
78 }
79
80
需要说明一下,就是在线表的删除问题。因为这个在线表是通过程序在数据库中进行创建的,
同时表中的olid又是一个自增字段(标识自增为1),因此为了避免自增字段最终超过最大值范围
(因为在线表经常有数据添加进来)。所以在上面加入了"olid > 2147483000"的逻辑判断,来预
防这个问题的出现。
说了一大堆的关于timeout参数的问题,而另一个重要的参数olid还没作详细介绍呢:)
上面所述的登陆页面(login.aspx.cs)中,olid是从basepage类中获得的,而basepage就是前
台主要页面的“基类”,它里面的构造函数部分封装了页面中大部分公共变量的初始化操作。所以要
了解olid,还要从basepage.cs中分析一把。
请看下面的代码段(摘自discuz.web.ui项目下的basepage.cs文件):
2 /// 当前用户的在线表ID
3 /// </summary>
4 protected internal int olid;
5
6 public BasePage() //构造函数
7 {
8
9 oluserinfo = OnlineUsers.UpdateInfo(config.Passwordkey, config.Onlinetimeout);
10
11 //password 可用于下面的userkey赋值,以实现用户退出操作时的识别认证
12 password = oluserinfo.Password;
13 if (password.Length > 16)
14 {
15 userkey = password.Substring(4, 8).Trim();
16 }
17 else
18 {
19 userkey = "";
20 }
21
22
23 olid = oluserinfo.Olid;
24
25 }
26
27
从代码中可以看出olid是通过louserinfo对象进行赋值的,而louserinfo对象是通过UpdateInfo
来进行实始化绑定的,所以我们还需要再看一下这个函数,相关代码段如下:
2 /// 用户在线信息维护。判断当前用户的身份(会员还是游客),是否在在线列表中存在,如果存在则更新会员的当前动,不存在则建立.
3 /// </summary>
4 /// <param name="passwordkey">论坛passwordkey</param>
5 /// <param name="timeout">在线超时时间</param>
6 /// <param name="passwd">用户密码</param>
7 public static OnlineUserInfo UpdateInfo(string passwordkey, int timeout, int uid, string passwd)
8 {
9
10 lock(SynObject)
11 {
12 OnlineUserInfo __onlineuser = new OnlineUserInfo();
13
14 string ip = DNTRequest.GetIP();
15 int userid = Utils.StrToInt(ForumUtils.GetCookie("userid"), uid);
16 string password = (passwd == string.Empty ? ForumUtils.GetCookiePassword(passwordkey) : ForumUtils.GetCookiePassword(passwd,passwordkey));
17
18 if (password.Length == 0)
19 {
20 userid = -1;
21 }
22 // 如果密码非Base64编码字符串则怀疑被非法篡改, 直接置身份为游客
23 else if (!Utils.IsBase64String(password))
24 {
25 userid = -1;
26 }
27
28 if (userid != -1)
29 {
30 __onlineuser = GetOnlineUser(userid,password);
31 if (__onlineuser != null)
32 {
33
34 if (__onlineuser.Ip != ip)
35 {
36 UpdateIP(__onlineuser.Olid, ip);
37
38 __onlineuser.Ip = ip;
39
40 return __onlineuser;
41 }
42 }
43 else
44 {
45
46 // 判断密码是否正确
47 userid = Users.CheckPassword(userid, password, false);
48 if (userid != -1)
49 {
50 DeleteRowsByIP(ip);
51 return CreateUser(userid, timeout);
52 }
53 else
54 {
55 // 如密码错误则在在线表中创建游客
56 __onlineuser = GetOnlineUserByIP(-1, ip);
57 if (__onlineuser == null)
58 {
59 return CreateGuestUser(timeout);
60 }
61 }
62 }
63
64 }
65 else
66 {
67 __onlineuser = GetOnlineUserByIP(-1, ip);
68 if (__onlineuser == null)
69 {
70 return CreateGuestUser(timeout);
71 }
72
73 }
74
75 __onlineuser.Lastupdatetime = Utils.GetDateTime();
76 return __onlineuser;
77
78 }
79
80 }
81
82
看到这里大家明白了吧,原来不管用户是否注册登陆,这个函数都会将一些信息更新或添加到在线
表中。即判断当前用户的身份(会员还是游客),是否在"在线列表"中存在,如果存在则更新会员的当前动作,
不存在(首次访问)则初始化相关(游客)用户信息。
下面再介绍一下action这个参数,它是结构(struct)类型,里面的ActionName动作名称和ActionD-
escription动作描述(目前未用上)是用于显示当前动作信息的。相应的显示效果如下图所示:
如果大家觉得没问题的话,下面再介绍一下注销(退出)操作,因为有登陆就会有退出。而注销这
块的操作也很简单,请看如下代码段(位于aspx/1/logout.aspx.cs文件):
2 {
3 protected override void ShowPage()
4 {
5
6
7 if (DNTRequest.GetString("userkey") == userkey)
8 {
9 AddMsgLine("已经清除了您的登录信息, 稍后您将以游客身份返回首页");
10 Users.UpdateOnlineTime(uid);
11 OnlineUsers.DeleteRows(olid);
12 ForumUtils.ClearUserCookie();
13 Utils.WriteCookie(Utils.GetTemplateCookieName(), "", -999999);
14 }
15 else
16 {
17 AddMsgLine("无法确定您的身份, 稍后返回首页");
18 }
19 }
20 }
21
22
代码中的userkey其实是一个用户password的一个子串(详情上面basepage.cs代码中的相应部分)。
而“OnlineUsers.DeleteRows(olid);”这行代码其实就是删除在线表中指定olid的用户信息了。因
为代码过于简单,只是一条SQL语句而已,所以就不在这里多费笔默了。
其实关于在线表有关的操作还有很多,大家不妨挖一下onlines.cs这个文件,从中会有更多的收获。
最后再介绍一下有关“最大在线人数”这个问题,其实这个功能主要是为了中小站长(服务器资源相
对紧张)提供的一项设置,旨在当用户在线人数达到一定数量时,拒绝其它的访问。相应的代码判断逻
辑如下(摘自basepage.cs文件):
2 {
3 onlineusercount = OnlineUsers.GetOnlineAllUserCount();
4 }
5 else
6 {
7 onlineusercount = OnlineUsers.GetCacheOnlineAllUserCount();
8 }
9 if (onlineusercount >= config.Maxonlines && useradminid != 1 && pagename != "login.aspx" && pagename != "logout.aspx")
10 {
11 ShowMessage("抱歉,目前访问人数太多,你暂时无法访问论坛.", 0);
12 return;
13 }
14
里面的GetOnlineAllUserCount()和GetCacheOnlineAllUserCount()均为返回在线人数的方法,而
config.Onlinetimeout的设置是在管理后台完成的,见下图:
好了,今天的文章就先写到这里了,希望大家不要感觉是在“云里雾里”的:),如果有问题欢迎
与我交流或去我们的官方站点(http://nt.discuz.net)反映问题。
我的email: daizhj@discuz.com, daizhj617595@126.com, daizhj@gmail.com
关键字:.net,discuz,disucznt,online,在线,在线用户
作者:代震军,daizhj