NetBox 2.7 使用手册

迁移:也是从 Session 开始

为了让用户完全透明,在跨服务器时毫无感觉,核心的问题其实就是数据的自由获取,用户可以在他希望的地方得到自己希望的数据,比如自己的用户信息,自己的购物小车。下面我将给出一个 NetBox 针对这个问题给出的解决方案,关于 NetBox 的 HTTP 服务器的创建可以参见创建一个 WEB 服务器,参考手册参见 HttpServerHttpServerHost。这个方案可以有限的移植到 iis 上面,但是无论时易用性和安全性都大大削弱,大家可以自己去尝试一下。

第一步,共享 Cookie

从前面的分析可以看出,迁移用户的第一个门槛是 Cookie 只能单个服务器独占,对于很多应用来说,可能在一个限定的域中由两个或者多个服务器,比如:www.netbox.cn 和 chat.netbox.cn,我们的第一个目标就是让 Session 的 Cookie 能够在整个域中有效,以便于下一步我们能够迁移 Session 数据。

NetBoxHttpServerHost 对象中,可以通过属性 SessionDomain 为虚拟主机设定 Session 的域范围,根据我们的例子,我们需要设定用户在 *.netbox.cn 的整个域中自由迁移,那么可以在创建各个服务器的 HTTP 服务器的虚拟主机时指定其 SessionDomain

Set Host = httpd.AddHost "www.netbox.cn", "\wwwroot"
Host.SessionDomain = ".NetBox.cn"

上面的代码创建一个虚拟主机,并设定其会话有效域为 ".NetBox.cn"

第二步,共享会话标识

通过上面的方法,你已经可以在相同域内的各个服务器上面访问其他服务器发出的会话 Cookie,但是仅仅这样还是不够的,我们希望能够直接在各个服务器之间相互识别对方的标识,即只需要一个服务器发出会话标识,而其他服务器则直接获取和使用这个会话标识。要达到这个目的,就需要让所有的服务器使用相同的主机识别字符串,也就是我们前面看见的 Cookie 的名称。

NetBox 中,我们可以修改 HttpServerHost 对象的 ApplicationID 属性来达到这个目的。通过将 ApplicationID 设定为固定的标识字符串,我们可以将各个单独服务器的主机标识统一起来,以达到共享会话标识的目的:

Host.ApplicationID = "MyNetBoxSession"

最终的应用标识名称叫什么无关紧要,只要一组服务器设定成相同的名称即可。

第三步,分析 Session

首先先澄清一个想法,我们并不是想在多个服务器之间共享 Session,那样会引入大量根本不是必须的服务器间操作,并且随着服务器增加这个浪费会呈指数上升。如果希望用户能够在多个服务器之间迁移,又不共享数据,那就需要同步。

我们可以按照上面已经有的模型来分析其中的关键。当一个用户在 www.netbox.cn 服务器上面登录,并拥有自己的私有数据,便拥有 netbox.cn 这个域的 Cookie,此时他访问 chat.netbox.cn 这个服务器,因为他在这个服务器上面并没有登录过,所以没有他的私有信息,但是服务器通过 Cookie 可以发现他已经拥有了一个会话标识,于是服务器便使用这个会话标识在本机为其创建了一个空的 Session 对象。现在的问题是,Session 里面的数据从哪里来。

第四步,保存 Session

为了让别的服务器能够获取指定用户的数据,就必须把用户的会话标识和用户关联起来,理所当然,我们首选数据库来完成这个任务。当然,如果你有一些服务器是在另外一个城市,不希望直接使用数据库操作,也可以选择其他的方式,“先进”的有 WebService,“后进”的也可以直接用 asp 程序实现一个简单小程序。

以数据库为例,在数据库中,我们创建一个名为 userlog 的表:

字段名 类型 长度 描述
SessionID char 40 用户的会话标识
UserID integer 用户ID,根据情况可以是其他的唯一标识
LogonTime datetime 用户的登录时间
UpdateTime datetime 用户会话更新时间
RefCount integer 引用用户会话的服务器个数,为 0 则表示无服务器引用,即为用户离开

我们的 logon.asp 除了做必要的登录之外,就需要多做一些事情:

<%If 认证成功 Then
    Session.Reset ;初始化会话
    ……… 你的登录代码

    conn.Execute("INSERT INTO userlog VALUES('" & Session.SessionKey & "', " &_
        userid & ", GETDATE(), GETDATE(), 1)")
End If
%>

在上面的代码中,使用了两个 Session 的方法和属性,一个是 Reset,用于建立一个新的会话,另外一个是 SessionKey 属性,用于查询当前会话的标识。

第五步,同步 Session

经过上面的步骤,我们已经能够从数据库中得知用户会话所对应的用户标识,当另外一个服务器得到这个会话,并创建了一个新的会话对象时,我们便可以在 Session 对象的初始化事件 OnSessionStart 中获取用户的私有信息:

Sub OnSessionStart
    ……
    conn.Execute("UPDATE userlog SET UpdateTime=GETDATE(), RefCount=RefCount+1 "&_
        "WHERE SessionID='" & Session.SessionKey & "' AND RefCount>0")
    Set rs = conn.Execute("SELECT * FROM userlog WHERE SessionID='" &_
        Session.SessionKey & "' AND RefCount>0")
    ……
End Sub

上面的代码增加会话的引用计数,修改更新时间,并返回会话的数据,以得到用户信息,得到用户 ID 以后的事情就不要我再罗嗦了吧。

而在服务器因为用户长时间未访问而放弃会话时,则需要减少引用计数:

Sub OnSessionEnd
    ……
    conn.Execute("UPDATE userlog SET UpdateTime=GETDATE(), RefCount=RefCount-1 "&_
        "WHERE SessionID='" & Session.SessionKey & "' AND RefCount>0")
    ……
End Sub

你也可以适当修改上面的 SQL 语句,减去 20 分钟的会话超时时间,以真实体现用户离线时间。

第六步,废弃 Session

在 logout.asp 中,我们也可能需要做不同处理。如果你直接使用 Session 对象的 Abandon 方法注销用户,但是因为你可能在 OnSessionEnd 事件中处理过离线时间,所以立即放弃会话可能导致时间上计算错误,所以你也可以用下面的代码来注销用户:

<%    ……
    conn.Execute("UPDATE userlog SET UpdateTime=GETDATE(), RefCount=RefCount-1 "&_
        "WHERE SessionID='" & Session.SessionKey & "' AND RefCount>0")
    Session.Reset
    ……
%>

而如果你根本不关心用户的实际下线时间,那直接使用 Abandon 也是比较方便的选择。

其他:关键修改

你可能允许用户修改了一些个性化的数据,而这些数据是被你缓存在 Session 里面的,那么你在修改了这些数据后就必须能够让其他已经缓存了的数据也同时废弃掉,此时,你可以使用上面注销用户一样的操作废弃当前 Session,然后重新装载数据,最后将新的会话信息保存到数据库中,以便于其他服务器同步数据:

<%    ……
    conn.Execute("UPDATE userlog SET UpdateTime=GETDATE(), RefCount=RefCount-1 "&_
        "WHERE SessionID='" & Session.SessionKey & "' AND RefCount>0")
    Session.Reset
    重新加载用户数据进 Session
    conn.Execute("INSERT INTO userlog VALUES('" & Session.SessionKey & "', " &_
        userid & ", GETDATE(), GETDATE(), 1)")
    ……
%>

稍微分析上面的代码,不难发现,所谓的关键个人信息修改,其实是自动帮助用户进行了一次退出和重新登录的操作。而在调用 SessionReset 方法前后,SessionKey 的内容已经发生了变化,用户拥有了一个新的会话,而正是因为有了这个细微的变化,其他的服务器才有可能捕获到会话的变化,从而自动更新数据。

只有需要与其他服务器同步更新的数据才需要这样操作,如果你所修改的 Session 数据不是需要迁移的数据,比如用户修改了自己的口令,或者仅仅是统计此用户在本服务器的访问页面数,此数字对其他服务器根本没有用处,则不需要做任何特殊处理。

前进:负载均衡

这个方案同样可以用于建立基于负载均衡的服务器群集,我们可以根本不需要拆分应用,而只是将全部应用复制到所有群集的服务器中,针对不同的用户,由不同的服务器提供服务,以达到分摊负载扩大应用的目的。

以 MS 的 WLBS 为例,在构架负载均衡的时候,我们可以设定均衡模式为针对 IP 的单一映射。如果不出现意外,某个用户会固定的由某一台服务器提供服务,当服务器意外停止(硬件故障、网络原因、电源断电等),其他服务器将自动接受此服务器的用户继续服务,而用户则对此毫无知觉。同时有一些网络服务商会采用一些网络技术,比如 PIX 技术,可以让用户共享地址,并且一个用户可能最终分配的外部地址也可能频繁变化,此时,提供服务的服务器也可能会不断变化,而自动 Session 同步技术同样也可以向用户透明这些操作。

你同样也可以将负载均衡设定为地址无关的方式,此时用户的请求会被根据自动分配到不同的服务器,由于均衡是以请求,而不是以用户为单位,群集将拥有更加平衡的负载。然而因为用户可能随机转移到不同的服务器上,也导致了用户会话的重复复制,随着服务器的增加,同步会话所引入的负载也会逐步增加。同时,因为 NetBox 与浏览器之间保持网络连接,对于某个用户来说,其连接的服务器比较固定,所以实际引入的会话同步负载也是极其有限的。

最终,你可以根据应用实际测试的情况选择负载均衡方式。负载均衡也不会是最后的构架方式,程序员可以随意组合负载均衡与拆分应用等各种方法扩张自己的系统,这一切都依赖于会话的迁移。

小结:下课了下课了

上面的方案并没有包含一些性能优化上的描述,比如数据库中的表建立什么样的索引,更新数据库前先判断是否是登录用户,如果不是则根本不需要访问数据库,等等等等,因为不是这个专题的主题,所以略去了。而且上面的代码也只是一个模型,要建立一个完善的可扩展应用,还需要你的努力。

经此一役,我们成功地从 Session 的牢笼中将用户释放了出来,我们的用户从此可以在我们任何一台服务器上面随意浏览,而我们的程序则只需要修改关键的部分代码,即可无限延伸开来,你有钱吗,有多少钱就能跑多大的应用。


版权所有: 2003- 网络盒子