如何在传统 ASP 和 ASP.NET 之间共享会话状态
Billy Yuen
Microsoft Corporation
2003 年 2 月
适用于:
Microsoft ASP.NET
摘要:讨论如何利用 Microsoft .NET 框架类和 .NET 框架的序列化特性,以便在传统 ASP 和 Microsoft ASP.NET 之间共享会话状态。通过共享会话状态,就允许在并行运行现有的 ASP 应用程序和 ASP.NET 应用程序的同时,分阶段地将 ASP 应用程序转换为 ASP.NET 应用程序。(12 页打印页)
本页内容
简介
概念综述
ASP.NET 实现
ASP 实现
演示程序
在现有的 ASP 应用程序中嵌入 COM 对象
局限性/改进
小结
简介
Microsoft ASP.NET 是最新的 Microsoft 技术,用于开发基于 Web 的应用程序。相比传统的 ASP 脚本技术而言,它具有很多优点,其中包括:1) 将 UI 表示形式从业务逻辑中分离出来,从而提供更好的开发结构; 2) 其代码是完全编译的,而在传统 ASP 中代码是解释的;和 3) 其编译特性结合其高速缓存支持,就意味着相对用传统 ASP 编写的等效站点而言,使用 ASP.NET 编写的站点的性能有显著提高。
尽管将现有的 ASP 应用程序转换到 ASP.NET 具有潜在的益处,但很多现有的 ASP 应用程序都具有关键的使命并且是相当复杂的。这种转换过程可能需要大量资源,并可能给现有的应用程序带来额外的风险。要解决这些问题,一种方法就是同时运行 ASP 和 ASP.NET,并一次只将应用程序的一部分转换为 ASP.NET。为了同时运行新的和旧的应用程序,就需要一种机制在传统 ASP 和 ASP.NET 之间共享会话状态。在本文中,我将讨论如何利用 Microsoft.NET 框架的若干类和序列化特性来共享这些会话状态。
概念综述
Cookie 是 Web 应用程序用来标识用户会话的最常用方法,可供传统 ASP 和 ASP.NET 二者用来标识会话状态。而用 ASP 脚本将会话状态信息存储在内存中,且不能与其他应用程序(如 ASP.NET)共享。如果会话状态以一种通用格式存储在 Microsoft SQL Server 中,则传统的 ASP 和 ASP.NET 都能访问会话状态。
在此示例中,使用了一个名为 mySession 的 cookie 来标识用户会话。当用户向 Web 应用程序发出请求时,该用户将被发放一个唯一的 cookie 以便标识该会话。在后续的请求中,浏览器将该唯一的 cookie 发送回服务器以标识该会话。在加载所请求的 Web 页面之前,一个自定义的对象将利用该唯一 cookie 从 SQL Server 中重新加载用户会话数据。在 Web 页面中通过该自定义的对象即可访问会话状态。在 Web 请求结束后,随着该请求的终止,会话数据将被保存回 SQL Server 中(参见图 1)。
ASP.NET 实现
在 ASP.NET 中,每个 Web 页面都是从 System.Web.UI.Page 类派生出来的。Page 类中包含 HttpSession 对象的一个实例以用于会话数据。在本示例中,从 System.Web.UI.Page 派生了一个名为 SessionPage 的自定义 Page 类,以实现与 Page 类完全相同的各种特性。派生页的唯一不同之处就是利用一个自定义的会话对象重写了默认的 HttpSession。(利用实例变量的 new 修饰符,C# 允许派生类隐藏基类的成员。)
public class SessionPage : System.Web.UI.Page { ... public new mySession Session = null; ... }
自定义的会话类负责利用 HybridDictionary 对象将会话状态存储到内存中。(HybridDictionary 能够高效地处理任何数量的会话元素。)为了实现与传统 ASP 之间的互操作性,该自定义的会话类将会话数据类型限定为仅允许字符串型。(默认的 HttpSession 允许将任何类型的数据存储在会话中,而这将不能与传统 ASP 互操作。)
[Serializable] public class mySession { private HybridDictionary dic = new HybridDictionary(); public mySession() { } public string this [string name] { get { return (string)dic[name.ToLower()]; } set { dic[name.ToLower()] = value; } } }
Page 类公开不同的事件和方法以供进行自定义。特别地,OnInit 方法用于设置 Page 对象的初始化状态。如果该请求不具有 mySession cookie,则将给请求者发放一个新的 mySession cookie。否则,将利用一个自定义的数据访问对象(SessionPersistence)从 SQL Server 中检索会话数据。dsn 和 SessionExpiration 值是从 web.config 中检索的。
override protected void OnInit(EventArgs e) { InitializeComponent(); base.OnInit(e); } private void InitializeComponent() { cookie = this.Request.Cookies[sessionPersistence.SessionID]; if (cookie == null) { Session = new mySession(); CreateNewSessionCookie(); IsNewSession = true; } else Session = sessionPersistence.LoadSession( Server.UrlDecode(cookie.Value).ToLower().Trim(), dsn, SessionExpiration ); this.Unload += new EventHandler(this.PersistSession); } private void CreateNewSessionCookie() { cookie = new HttpCookie(sessionPersistence.SessionID, sessionPersistence.GenerateKey()); this.Response.Cookies.Add(cookie); }
为了获得最佳性能,SessionPersistence 类利用 Microsoft .NET 框架的 BinaryFormatter,以二进制格式对会话状态进行序列化和反序列化。随后,可以将所得到的二进制会话状态数据以 art 字段类型存储在 SQL Server 中。
public mySession LoadSession(string key, string dsn, int SessionExpiration) { SqlConnection conn = new SqlConnection(dsn); SqlCommand LoadCmd = new SqlCommand(); LoadCmd.CommandText = command; LoadCmd.Connection = conn; SqlDataReader reader = null; mySession Session = null; try { LoadCmd.Parameters.Add("@ID", new Guid(key)); conn.Open(); reader = LoadCmd.ExecuteReader(); if (reader.Read()) { DateTime LastAccessed = reader.GetDateTime(1).AddMinutes(SessionExpiration); if (LastAccessed >= DateTime.Now) Session = Deserialize((Byte[])reader["Data"]); } } finally { if (reader != null) reader.Close(); if (conn != null) conn.Close(); } return Session; } private mySession Deserialize(Byte[] state) { if (state == null) return null; mySession Session = null; Stream stream = null; try { stream = new MemoryStream(); stream.Write(state, 0, state.Length); stream.Position = 0; IFormatter formatter = new BinaryFormatter(); Session = (mySession)formatter.Deserialize(stream); } finally { if (stream != null) stream.Close(); } return Session; }
当该请求结束时,将激发 Page 类的 Unload 事件,注册用于 Unload 事件的事件处理程序将会话数据序列化成二进制格式,并将所得的二进制数据保存到 SQL Server 中。
private void PersistSession(Object obj, System.EventArgs arg) { sessionPersistence.SaveSession( Server.UrlDecode(cookie.Value).ToLower().Trim(), dsn, Session, IsNewSession); } public void SaveSession(string key, string dsn, mySession Session, bool IsNewSession) { SqlConnection conn = new SqlConnection(dsn); SqlCommand SaveCmd = new SqlCommand(); SaveCmd.Connection = conn; try { if (IsNewSession) SaveCmd.CommandText = InsertStatement; else SaveCmd.CommandText = UpdateStatement; SaveCmd.Parameters.Add("@ID", new Guid(key)); SaveCmd.Parameters.Add("@Data", Serialize(Session)); SaveCmd.Parameters.Add("@LastAccessed", DateTime.Now.ToString()); conn.Open(); SaveCmd.ExecuteNonQuery(); } finally { if (conn != null) conn.Close(); } } private Byte[] Serialize(mySession Session) { if (Session == null) return null; Stream stream = null; Byte[] state = null; try { IFormatter formatter = new BinaryFormatter(); stream = new MemoryStream(); formatter.Serialize(stream, Session); state = new Byte[stream.Length]; stream.Position = 0; stream.Read(state, 0, (int)stream.Length); stream.Close(); } finally { if (stream != null) stream.Close(); } return state; }
SessionPage 类及其相关类都封装在 SessionUtility 程序集中。在新的 ASP.NET 项目中,将建立一个对该 SessionUtility 程序集的引用,并且为了与传统 ASP 代码共享会话,将从 SessionPage 而不是 Page 类派生出每个页面。一旦完成迁移过程,通过注释掉 SessionPage 类中的 Session 变量声明即可解除基类 HttpSession 的隐藏,从而新的应用程序可切换回使用本机的 HttpSession 对象。
ASP 实现
本机的 ASP 会话只能将会话数据存储在内存中。为了将会话数据存储到 SQL Server 中,我们编写了一个自定义的 Microsoft Visual Basic6.0 COM 对象以管理会话状态,而不使用本机的会话对象进行管理。这个 COM 对象将在每个 Web 请求开始时得以实例化,并从 SQL Server 处重新加载会话数据。当 ASP 脚本完成时,此对象将终止,并且会话状态将被保存回 SQL Server 中。
Visual Basic 6 COM Session 对象的主要目的就是提供对 Microsoft Internet Information Server 内部对象的访问。Visual Basic 6.0 COM Session 对象使用 SessionUtility 程序集的 mySession 类来保留会话状态,并使用 SessionUtility 的 SessionPersistence 类从 SQL Server 中加载会话数据或将会话数据保存回 SQL Server。利用 regasm.exe 实用工具,mySession 和 SessionPersistence 类可被公开为 COM 对象。regasm.exe 实用工具能够注册并创建一个类库,以便 COM 客户端使用各个框架类。
在该对象的构造过程中,会话状态信息得以重新加载。构造函数 (class_initialize) 将首先从 Application 对象中检索会话 cookie、会话超时 (SessionTimeOut) 和数据库连接字符串 (SessionDSN),并创建 mySession 类的一个实例以持有这些会话数据。然后,构造函数将尝试利用给定的 cookie 从 SQL Server 中重新加载会话数据。如果 SQL Server 不包含相应的会话信息,或者该会话已经过期,则将发放一个新的 cookie。如果 SQL Sever 确实返回会话状态数据,则这些会话状态将被存储在 mySession 对象中。
Private Sub Class_Initialize() On Error GoTo ErrHandler: Const METHOD_NAME As String = "Class_Initialize" Set mySessionPersistence = New SessionPersistence Set myObjectContext = GetObjectContext() mySessionID = ReadSessionID() myDSNString = GetConnectionDSN() myTimeOut = GetSessionTimeOut() myIsNewSession = False Call InitContents Exit Sub ErrHandler: Err.Raise Err.Number, METHOD_NAME & ":" & Err.Source, Err.Description End Sub Private Sub InitContents() On Error GoTo ErrHandler: Const METHOD_NAME As String = "InitContents" If mySessionID = "" Then Set myContentsEntity = New mySession mySessionID = mySessionPersistence.GenerateKey myIsNewSession = True Else Set myContentsEntity = mySessionPersistence.LoadSession(mySessionID, myDSNString, myTimeOut) End If Exit Sub ErrHandler: Err.Raise Err.Number, METHOD_NAME & ":" & Err.Source, Err.Description End Sub
当该对象实例超出脚本的作用范围时,析构函数 (class_terminate) 将执行。析构函数将利用 SessionPersistence.SaveSession() 方法保持会话数据。如果这是新会话,析构函数还会向浏览器回送一个新 cookie。
Private Sub Class_Terminate() On Error GoTo ErrHandler: Const METHOD_NAME As String = "Class_Terminate" Call SetDataForSessionID Exit Sub ErrHandler: Err.Raise Err.Number, METHOD_NAME & ":" & Err.Source, Err.Description> End Sub Private Sub SetDataForSessionID() On Error GoTo ErrHandler: Const METHOD_NAME As String = "SetDataForSessionID" Call mySessionPersistence.SaveSession(mySessionID, myDSNString, myContentsEntity, myIsNewSession) If myIsNewSession Then Call WriteSessionID(mySessionID) Set myContentsEntity = Nothing Set myObjectContext = Nothing Set mySessionPersistence = Nothing Exit Sub ErrHandler: Err.Raise Err.Number, METHOD_NAME & ":" & Err.Source, Err.Description End Sub
单击本文顶部的链接,您可以下载 ASP.NET SessionUtility 项目的源代码 — COM 会话管理器和演示代码。
演示程序
本演示程序的设计目的为递增并显示一个数字。不管加载哪个页面,该数字将总是递增,因为其数值存储在 SQL Server 中且在传统 ASP 和 ASP.NET 之间共享。
演示程序的设置步骤
-
创建一个名为 SessionDemoDb 的新数据库。
-
创建 SessState 表 (osql.exe –E –d SessionDemoDb –i Session.sql)。
-
创建名为 Demo 的新虚拟目录。
-
关闭 ASP 配置选项卡中的 ASP Session。
-
将 web.config、testPage.aspx、Global.asa、testPage.asp 和 GlobalInclude.asp 复制到虚拟目录中。
-
更新 Global.asa 和 web.config 中的 DSN 字符串设置。会话超时设置是可选的。默认值为 20 分钟。聽
-
将 SessionUtility.dll 安装到 Global Assembly Cache (gacutil /i SessionUtility.dll)。
-
利用 regasm.exe 将 SessionUtility.dll 公开为 COM 对象 (regasm.exe SessionUtility.dll /tlb:SessionUtility.tlb)。
-
将 SessionManager.dll 复制到一个本地目录中,并利用 regsvr32.exe 注册该文件 (regsvr32 SessionManager.dll)。
-
为 IUSR_<machine_name> 帐号赋予对 SessionMgr.dll 的读和执行权限。
演示程序的运行步骤
-
启动 Microsoft Internet Explorer。
-
加载传统 ASP 的 testPage.asp。Web 页面中应该显示数字 "1"。
-
单击 Internet Explorer 上的刷新按钮,重新加载该页面。该数字应该递增。
-
将 URL 改为 ASP.NET 版的 testPage.aspx。该数字应该继续递增。
-
如果首先启动 testPage.aspx 页面,也可重复同样的过程。
在现有的 ASP 应用程序中嵌入 COM 对象
在开发 ASP 应用程序时,惯例是在每个脚本的开始处包含一个文件以便共享公共代码和常量。要加入自定义的会话对象,最佳的方法就是在公共的包含文件中添加相应的实例化代码。最后一个步骤就是将对该会话对象的全部引用替换为自定义的会话变量名。
局限性/改进
如果现有的 ASP 应用程序将一个 COM 对象存储在 Session 对象中,则此解决方案并不支持这种情况。在这种情况下,需要一个自定义的封送拆收器来序列化/反序列化各种状态,以便使用自定义的会话对象。此外,此解决方案不支持存储字符串类型数组。但只需稍加努力,我们就可利用 Microsoft Visual Basic6.0 Join 函数将所有的数组元素组合成单个字符串,然后再将其存入会话对象中,从而实现这种功能。利用 Visual Basic 6.0 Split 函数将该字符串分解成单独的数组元素即可完成反向操作。在 .NET 框架方面,Join 和 Split 方法都是 String 类的成员。
小结
ASP.NET 代表了一种全新的编程典范和结构,并且比传统的 ASP 具有更多优势。虽然从 ASP 迁移到 ASP.NET 并不是一个简单的过程,但 ASP.NET 更好的编程模型和更高的性能使得这种转换过程物有所值。除了将 COM 对象存储在 Session 对象中的情况外,本文所述的方法提供了一种解决方案,使得这种迁移过程更加简单。
关于作者
Billy Yuen 就职于北加州的 Microsoft 硅谷技术中心。此中心致力于开发 Microsoft .NET 框架解决方案。如果希望与他联系,可发送电子邮件至billyy@microsoft.com。